| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962 |
- # 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 collections
- import itertools
- from collections.abc import Iterator
- from typing import TYPE_CHECKING, Literal, cast
- import astroid
- from astroid import bases, nodes, objects, util
- from pylint import utils as lint_utils
- from pylint.checkers import BaseChecker, utils
- from pylint.interfaces import HIGH, INFERENCE, Confidence
- from pylint.reporters.ureports import nodes as reporter_nodes
- from pylint.utils import LinterStats
- if TYPE_CHECKING:
- from pylint.lint.pylinter import PyLinter
- class _BasicChecker(BaseChecker):
- """Permits separating multiple checks with the same checker name into
- classes/file.
- """
- name = "basic"
- REVERSED_PROTOCOL_METHOD = "__reversed__"
- SEQUENCE_PROTOCOL_METHODS = ("__getitem__", "__len__")
- REVERSED_METHODS = (SEQUENCE_PROTOCOL_METHODS, (REVERSED_PROTOCOL_METHOD,))
- # A mapping from qname -> symbol, to be used when generating messages
- # about dangerous default values as arguments
- DEFAULT_ARGUMENT_SYMBOLS = dict(
- zip(
- [".".join(["builtins", x]) for x in ("set", "dict", "list")],
- ["set()", "{}", "[]"],
- ),
- **{
- x: f"{x}()"
- for x in (
- "collections.deque",
- "collections.ChainMap",
- "collections.Counter",
- "collections.OrderedDict",
- "collections.defaultdict",
- "collections.UserDict",
- "collections.UserList",
- )
- },
- )
- def report_by_type_stats(
- sect: reporter_nodes.Section,
- stats: LinterStats,
- old_stats: LinterStats | None,
- ) -> None:
- """Make a report of.
- * percentage of different types documented
- * percentage of different types with a bad name
- """
- # percentage of different types documented and/or with a bad name
- nice_stats: dict[str, dict[str, str]] = {}
- for node_type in ("module", "class", "method", "function"):
- total = stats.get_node_count(node_type)
- nice_stats[node_type] = {}
- if total != 0:
- undocumented_node = stats.get_undocumented(node_type)
- documented = total - undocumented_node
- percent = (documented * 100.0) / total
- nice_stats[node_type]["percent_documented"] = f"{percent:.2f}"
- badname_node = stats.get_bad_names(node_type)
- percent = (badname_node * 100.0) / total
- nice_stats[node_type]["percent_badname"] = f"{percent:.2f}"
- lines = ["type", "number", "old number", "difference", "%documented", "%badname"]
- for node_type in ("module", "class", "method", "function"):
- node_type = cast(Literal["function", "class", "method", "module"], node_type)
- new = stats.get_node_count(node_type)
- old = old_stats.get_node_count(node_type) if old_stats else None
- diff_str = lint_utils.diff_string(old, new) if old else None
- lines += [
- node_type,
- str(new),
- str(old) if old else "NC",
- diff_str if diff_str else "NC",
- nice_stats[node_type].get("percent_documented", "0"),
- nice_stats[node_type].get("percent_badname", "0"),
- ]
- sect.append(reporter_nodes.Table(children=lines, cols=6, rheaders=1))
- # pylint: disable-next = too-many-public-methods
- class BasicChecker(_BasicChecker):
- """Basic checker.
- Checks for :
- * doc strings
- * number of arguments, local variables, branches, returns and statements in
- functions, methods
- * required module attributes
- * dangerous default values as arguments
- * redefinition of function / method / class
- * uses of the global statement
- """
- name = "basic"
- msgs = {
- "W0101": (
- "Unreachable code",
- "unreachable",
- 'Used when there is some code behind a "return" or "raise" '
- "statement, which will never be accessed.",
- ),
- "W0102": (
- "Dangerous default value %s as argument",
- "dangerous-default-value",
- "Used when a mutable value as list or dictionary is detected in "
- "a default value for an argument.",
- ),
- "W0104": (
- "Statement seems to have no effect",
- "pointless-statement",
- "Used when a statement doesn't have (or at least seems to) any effect.",
- ),
- "W0105": (
- "String statement has no effect",
- "pointless-string-statement",
- "Used when a string is used as a statement (which of course "
- "has no effect). This is a particular case of W0104 with its "
- "own message so you can easily disable it if you're using "
- "those strings as documentation, instead of comments.",
- ),
- "W0106": (
- 'Expression "%s" is assigned to nothing',
- "expression-not-assigned",
- "Used when an expression that is not a function call is assigned "
- "to nothing. Probably something else was intended.",
- ),
- "W0108": (
- "Lambda may not be necessary",
- "unnecessary-lambda",
- "Used when the body of a lambda expression is a function call "
- "on the same argument list as the lambda itself; such lambda "
- "expressions are in all but a few cases replaceable with the "
- "function being called in the body of the lambda.",
- ),
- "W0109": (
- "Duplicate key %r in dictionary",
- "duplicate-key",
- "Used when a dictionary expression binds the same key multiple times.",
- ),
- "W0122": (
- "Use of exec",
- "exec-used",
- "Raised when the 'exec' statement is used. It's dangerous to use this "
- "function for a user input, and it's also slower than actual code in "
- "general. This doesn't mean you should never use it, but you should "
- "consider alternatives first and restrict the functions available.",
- ),
- "W0123": (
- "Use of eval",
- "eval-used",
- 'Used when you use the "eval" function, to discourage its '
- "usage. Consider using `ast.literal_eval` for safely evaluating "
- "strings containing Python expressions "
- "from untrusted sources.",
- ),
- "W0150": (
- "%s statement in finally block may swallow exception",
- "lost-exception",
- "Used when a break or a return statement is found inside the "
- "finally clause of a try...finally block: the exceptions raised "
- "in the try clause will be silently swallowed instead of being "
- "re-raised.",
- ),
- "W0199": (
- "Assert called on a populated tuple. Did you mean 'assert x,y'?",
- "assert-on-tuple",
- "A call of assert on a tuple will always evaluate to true if "
- "the tuple is not empty, and will always evaluate to false if "
- "it is.",
- ),
- "W0124": (
- 'Following "as" with another context manager looks like a tuple.',
- "confusing-with-statement",
- "Emitted when a `with` statement component returns multiple values "
- "and uses name binding with `as` only for a part of those values, "
- "as in with ctx() as a, b. This can be misleading, since it's not "
- "clear if the context manager returns a tuple or if the node without "
- "a name binding is another context manager.",
- ),
- "W0125": (
- "Using a conditional statement with a constant value",
- "using-constant-test",
- "Emitted when a conditional statement (If or ternary if) "
- "uses a constant value for its test. This might not be what "
- "the user intended to do.",
- ),
- "W0126": (
- "Using a conditional statement with potentially wrong function or method call due to "
- "missing parentheses",
- "missing-parentheses-for-call-in-test",
- "Emitted when a conditional statement (If or ternary if) "
- "seems to wrongly call a function due to missing parentheses",
- ),
- "W0127": (
- "Assigning the same variable %r to itself",
- "self-assigning-variable",
- "Emitted when we detect that a variable is assigned to itself",
- ),
- "W0128": (
- "Redeclared variable %r in assignment",
- "redeclared-assigned-name",
- "Emitted when we detect that a variable was redeclared in the same assignment.",
- ),
- "E0111": (
- "The first reversed() argument is not a sequence",
- "bad-reversed-sequence",
- "Used when the first argument to reversed() builtin "
- "isn't a sequence (does not implement __reversed__, "
- "nor __getitem__ and __len__",
- ),
- "E0119": (
- "format function is not called on str",
- "misplaced-format-function",
- "Emitted when format function is not called on str object. "
- 'e.g doing print("value: {}").format(123) instead of '
- 'print("value: {}".format(123)). This might not be what the user '
- "intended to do.",
- ),
- "W0129": (
- "Assert statement has a string literal as its first argument. The assert will %s fail.",
- "assert-on-string-literal",
- "Used when an assert statement has a string literal as its first argument, which will "
- "cause the assert to always pass.",
- ),
- "W0130": (
- "Duplicate value %r in set",
- "duplicate-value",
- "This message is emitted when a set contains the same value two or more times.",
- ),
- "W0131": (
- "Named expression used without context",
- "named-expr-without-context",
- "Emitted if named expression is used to do a regular assignment "
- "outside a context like if, for, while, or a comprehension.",
- ),
- "W0133": (
- "Exception statement has no effect",
- "pointless-exception-statement",
- "Used when an exception is created without being assigned, raised or returned "
- "for subsequent use elsewhere.",
- ),
- "W0134": (
- "'return' shadowed by the 'finally' clause.",
- "return-in-finally",
- "Emitted when a 'return' statement is found in a 'finally' block. This will overwrite "
- "the return value of a function and should be avoided.",
- ),
- }
- reports = (("RP0101", "Statistics by type", report_by_type_stats),)
- def __init__(self, linter: PyLinter) -> None:
- super().__init__(linter)
- self._trys: list[nodes.Try]
- def open(self) -> None:
- """Initialize visit variables and statistics."""
- py_version = self.linter.config.py_version
- self._py38_plus = py_version >= (3, 8)
- self._trys = []
- self.linter.stats.reset_node_count()
- @utils.only_required_for_messages(
- "using-constant-test", "missing-parentheses-for-call-in-test"
- )
- def visit_if(self, node: nodes.If) -> None:
- self._check_using_constant_test(node, node.test)
- @utils.only_required_for_messages(
- "using-constant-test", "missing-parentheses-for-call-in-test"
- )
- def visit_ifexp(self, node: nodes.IfExp) -> None:
- self._check_using_constant_test(node, node.test)
- @utils.only_required_for_messages(
- "using-constant-test", "missing-parentheses-for-call-in-test"
- )
- def visit_comprehension(self, node: nodes.Comprehension) -> None:
- if node.ifs:
- for if_test in node.ifs:
- self._check_using_constant_test(node, if_test)
- def _check_using_constant_test(
- self,
- node: nodes.If | nodes.IfExp | nodes.Comprehension,
- test: nodes.NodeNG | None,
- ) -> None:
- const_nodes = (
- nodes.Module,
- nodes.GeneratorExp,
- nodes.Lambda,
- nodes.FunctionDef,
- nodes.ClassDef,
- bases.Generator,
- astroid.UnboundMethod,
- astroid.BoundMethod,
- nodes.Module,
- )
- structs = (nodes.Dict, nodes.Tuple, nodes.Set, nodes.List)
- # These nodes are excepted, since they are not constant
- # values, requiring a computation to happen.
- except_nodes = (
- nodes.Call,
- nodes.BinOp,
- nodes.BoolOp,
- nodes.UnaryOp,
- nodes.Subscript,
- )
- inferred = None
- emit = isinstance(test, (nodes.Const, *structs, *const_nodes))
- maybe_generator_call = None
- if not isinstance(test, except_nodes):
- inferred = utils.safe_infer(test)
- if isinstance(inferred, util.UninferableBase) and isinstance(
- test, nodes.Name
- ):
- emit, maybe_generator_call = BasicChecker._name_holds_generator(test)
- # Emit if calling a function that only returns GeneratorExp (always tests True)
- elif isinstance(test, nodes.Call):
- maybe_generator_call = test
- if maybe_generator_call:
- inferred_call = utils.safe_infer(maybe_generator_call.func)
- if isinstance(inferred_call, nodes.FunctionDef):
- # Can't use all(x) or not any(not x) for this condition, because it
- # will return True for empty generators, which is not what we want.
- all_returns_were_generator = None
- for return_node in inferred_call._get_return_nodes_skip_functions():
- if not isinstance(return_node.value, nodes.GeneratorExp):
- all_returns_were_generator = False
- break
- all_returns_were_generator = True
- if all_returns_were_generator:
- self.add_message(
- "using-constant-test", node=node, confidence=INFERENCE
- )
- return
- if emit:
- self.add_message("using-constant-test", node=test, confidence=INFERENCE)
- elif isinstance(inferred, const_nodes):
- # If the constant node is a FunctionDef or Lambda then
- # it may be an illicit function call due to missing parentheses
- call_inferred = None
- try:
- # Just forcing the generator to infer all elements.
- # astroid.exceptions.InferenceError are false positives
- # see https://github.com/pylint-dev/pylint/pull/8185
- if isinstance(inferred, (nodes.FunctionDef, nodes.Lambda)):
- call_inferred = list(inferred.infer_call_result(node))
- except astroid.InferenceError:
- call_inferred = None
- if call_inferred:
- self.add_message(
- "missing-parentheses-for-call-in-test",
- node=test,
- confidence=INFERENCE,
- )
- self.add_message("using-constant-test", node=test, confidence=INFERENCE)
- @staticmethod
- def _name_holds_generator(test: nodes.Name) -> tuple[bool, nodes.Call | None]:
- """Return whether `test` tests a name certain to hold a generator, or optionally
- a call that should be then tested to see if *it* returns only generators.
- """
- assert isinstance(test, nodes.Name)
- emit = False
- maybe_generator_call = None
- lookup_result = test.frame().lookup(test.name)
- if not lookup_result:
- return emit, maybe_generator_call
- maybe_generator_assigned = (
- isinstance(assign_name.parent.value, nodes.GeneratorExp)
- for assign_name in lookup_result[1]
- if isinstance(assign_name.parent, nodes.Assign)
- )
- first_item = next(maybe_generator_assigned, None)
- if first_item is not None:
- # Emit if this variable is certain to hold a generator
- if all(itertools.chain((first_item,), maybe_generator_assigned)):
- emit = True
- # If this variable holds the result of a call, save it for next test
- elif (
- len(lookup_result[1]) == 1
- and isinstance(lookup_result[1][0].parent, nodes.Assign)
- and isinstance(lookup_result[1][0].parent.value, nodes.Call)
- ):
- maybe_generator_call = lookup_result[1][0].parent.value
- return emit, maybe_generator_call
- def visit_module(self, _: nodes.Module) -> None:
- """Check module name, docstring and required arguments."""
- self.linter.stats.node_count["module"] += 1
- def visit_classdef(self, _: nodes.ClassDef) -> None:
- """Check module name, docstring and redefinition
- increment branch counter.
- """
- self.linter.stats.node_count["klass"] += 1
- @utils.only_required_for_messages(
- "pointless-statement",
- "pointless-exception-statement",
- "pointless-string-statement",
- "expression-not-assigned",
- "named-expr-without-context",
- )
- def visit_expr(self, node: nodes.Expr) -> None:
- """Check for various kind of statements without effect."""
- expr = node.value
- if isinstance(expr, nodes.Const) and isinstance(expr.value, str):
- # treat string statement in a separated message
- # Handle PEP-257 attribute docstrings.
- # An attribute docstring is defined as being a string right after
- # an assignment at the module level, class level or __init__ level.
- scope = expr.scope()
- if isinstance(scope, (nodes.ClassDef, nodes.Module, nodes.FunctionDef)):
- if isinstance(scope, nodes.FunctionDef) and scope.name != "__init__":
- pass
- else:
- sibling = expr.previous_sibling()
- if (
- sibling is not None
- and sibling.scope() is scope
- and isinstance(
- sibling, (nodes.Assign, nodes.AnnAssign, nodes.TypeAlias)
- )
- ):
- return
- self.add_message("pointless-string-statement", node=node)
- return
- # Warn W0133 for exceptions that are used as statements
- if isinstance(expr, nodes.Call):
- name = ""
- if isinstance(expr.func, nodes.Name):
- name = expr.func.name
- elif isinstance(expr.func, nodes.Attribute):
- name = expr.func.attrname
- # Heuristic: only run inference for names that begin with an uppercase char
- # This reduces W0133's coverage, but retains acceptable runtime performance
- # For more details, see: https://github.com/pylint-dev/pylint/issues/8073
- inferred = utils.safe_infer(expr) if name[:1].isupper() else None
- if isinstance(inferred, objects.ExceptionInstance):
- self.add_message(
- "pointless-exception-statement", node=node, confidence=INFERENCE
- )
- return
- # Ignore if this is :
- # * the unique child of a try/except body
- # * a yield statement
- # * an ellipsis (which can be used on Python 3 instead of pass)
- # warn W0106 if we have any underlying function call (we can't predict
- # side effects), else pointless-statement
- if (
- isinstance(expr, (nodes.Yield, nodes.Await))
- or (
- isinstance(node.parent, (nodes.Try, nodes.TryStar))
- and node.parent.body == [node]
- )
- or (isinstance(expr, nodes.Const) and expr.value is Ellipsis)
- ):
- return
- if isinstance(expr, nodes.NamedExpr):
- self.add_message("named-expr-without-context", node=node, confidence=HIGH)
- elif any(expr.nodes_of_class(nodes.Call)):
- self.add_message(
- "expression-not-assigned", node=node, args=expr.as_string()
- )
- else:
- self.add_message("pointless-statement", node=node)
- @staticmethod
- def _filter_vararg(
- node: nodes.Lambda, call_args: list[nodes.NodeNG]
- ) -> Iterator[nodes.NodeNG]:
- # Return the arguments for the given call which are
- # not passed as vararg.
- for arg in call_args:
- if isinstance(arg, nodes.Starred):
- if (
- isinstance(arg.value, nodes.Name)
- and arg.value.name != node.args.vararg
- ):
- yield arg
- else:
- yield arg
- @staticmethod
- def _has_variadic_argument(
- args: list[nodes.Starred | nodes.Keyword], variadic_name: str
- ) -> bool:
- return not args or any(
- (isinstance(a.value, nodes.Name) and a.value.name != variadic_name)
- or not isinstance(a.value, nodes.Name)
- for a in args
- )
- @utils.only_required_for_messages("unnecessary-lambda")
- def visit_lambda(self, node: nodes.Lambda) -> None:
- """Check whether the lambda is suspicious."""
- # if the body of the lambda is a call expression with the same
- # argument list as the lambda itself, then the lambda is
- # possibly unnecessary and at least suspicious.
- if node.args.defaults:
- # If the arguments of the lambda include defaults, then a
- # judgment cannot be made because there is no way to check
- # that the defaults defined by the lambda are the same as
- # the defaults defined by the function called in the body
- # of the lambda.
- return
- call = node.body
- if not isinstance(call, nodes.Call):
- # The body of the lambda must be a function call expression
- # for the lambda to be unnecessary.
- return
- match call.func:
- case nodes.Attribute(expr=nodes.Call()):
- # Chained call, the intermediate call might
- # return something else (but we don't check that, yet).
- return
- ordinary_args = list(node.args.args)
- new_call_args = list(self._filter_vararg(node, call.args))
- if node.args.kwarg:
- if self._has_variadic_argument(call.keywords, node.args.kwarg):
- return
- elif call.keywords:
- return
- if node.args.vararg:
- if self._has_variadic_argument(call.starargs, node.args.vararg):
- return
- elif call.starargs:
- return
- # The "ordinary" arguments must be in a correspondence such that:
- # ordinary_args[i].name == call.args[i].name.
- if len(ordinary_args) != len(new_call_args):
- return
- for arg, passed_arg in zip(ordinary_args, new_call_args):
- if not isinstance(passed_arg, nodes.Name):
- return
- if arg.name != passed_arg.name:
- return
- # The lambda is necessary if it uses its parameter in the function it is
- # calling in the lambda's body
- # e.g. lambda foo: (func1 if foo else func2)(foo)
- for name in call.func.nodes_of_class(nodes.Name):
- if name.lookup(name.name)[0] is node:
- return
- self.add_message("unnecessary-lambda", line=node.fromlineno, node=node)
- @utils.only_required_for_messages("dangerous-default-value")
- def visit_functiondef(self, node: nodes.FunctionDef) -> None:
- """Check function name, docstring, arguments, redefinition,
- variable names, max locals.
- """
- if node.is_method():
- self.linter.stats.node_count["method"] += 1
- else:
- self.linter.stats.node_count["function"] += 1
- self._check_dangerous_default(node)
- visit_asyncfunctiondef = visit_functiondef
- def _check_dangerous_default(self, node: nodes.FunctionDef) -> None:
- """Check for dangerous default values as arguments."""
- def is_iterable(internal_node: nodes.NodeNG) -> bool:
- return isinstance(internal_node, (nodes.List, nodes.Set, nodes.Dict))
- defaults = (node.args.defaults or []) + (node.args.kw_defaults or [])
- for default in defaults:
- if not default:
- continue
- try:
- value = next(default.infer())
- except astroid.InferenceError:
- continue
- if (
- isinstance(value, astroid.Instance)
- and value.qname() in DEFAULT_ARGUMENT_SYMBOLS
- ):
- if value is default:
- msg = DEFAULT_ARGUMENT_SYMBOLS[value.qname()]
- elif isinstance(value, astroid.Instance) or is_iterable(value):
- # We are here in the following situation(s):
- # * a dict/set/list/tuple call which wasn't inferred
- # to a syntax node ({}, () etc.). This can happen
- # when the arguments are invalid or unknown to
- # the inference.
- # * a variable from somewhere else, which turns out to be a list
- # or a dict.
- if is_iterable(default):
- msg = value.pytype()
- elif isinstance(default, nodes.Call):
- msg = f"{value.name}() ({value.qname()})"
- else:
- msg = f"{default.as_string()} ({value.qname()})"
- else:
- # this argument is a name
- msg = f"{default.as_string()} ({DEFAULT_ARGUMENT_SYMBOLS[value.qname()]})"
- self.add_message("dangerous-default-value", node=node, args=(msg,))
- @utils.only_required_for_messages("unreachable", "lost-exception")
- def visit_return(self, node: nodes.Return) -> None:
- """Return node visitor.
- 1 - check if the node has a right sibling (if so, that's some
- unreachable code)
- 2 - check if the node is inside the 'finally' clause of a 'try...finally'
- block
- """
- self._check_unreachable(node)
- # Is it inside final body of a try...finally block ?
- self._check_not_in_finally(node, "return", (nodes.FunctionDef,))
- @utils.only_required_for_messages("unreachable")
- def visit_continue(self, node: nodes.Continue) -> None:
- """Check is the node has a right sibling (if so, that's some unreachable
- code).
- """
- self._check_unreachable(node)
- @utils.only_required_for_messages("unreachable", "lost-exception")
- def visit_break(self, node: nodes.Break) -> None:
- """Break node visitor.
- 1 - check if the node has a right sibling (if so, that's some
- unreachable code)
- 2 - check if the node is inside the 'finally' clause of a 'try...finally'
- block
- """
- # 1 - Is it right sibling ?
- self._check_unreachable(node)
- # 2 - Is it inside final body of a try...finally block ?
- self._check_not_in_finally(node, "break", (nodes.For, nodes.While))
- @utils.only_required_for_messages("unreachable")
- def visit_raise(self, node: nodes.Raise) -> None:
- """Check if the node has a right sibling (if so, that's some unreachable
- code).
- """
- self._check_unreachable(node)
- def _check_misplaced_format_function(self, call_node: nodes.Call) -> None:
- if not isinstance(call_node.func, nodes.Attribute):
- return
- if call_node.func.attrname != "format":
- return
- expr = utils.safe_infer(call_node.func.expr)
- if isinstance(expr, util.UninferableBase):
- return
- if not expr:
- # we are doubtful on inferred type of node, so here just check if format
- # was called on print()
- match call_node.func.expr:
- case nodes.Call(func=nodes.Name(name="print")):
- self.add_message("misplaced-format-function", node=call_node)
- @utils.only_required_for_messages(
- "eval-used",
- "exec-used",
- "bad-reversed-sequence",
- "misplaced-format-function",
- "unreachable",
- )
- def visit_call(self, node: nodes.Call) -> None:
- """Visit a Call node."""
- if utils.is_terminating_func(node):
- self._check_unreachable(node, confidence=INFERENCE)
- self._check_misplaced_format_function(node)
- if isinstance(node.func, nodes.Name):
- name = node.func.name
- # ignore the name if it's not a builtin (i.e. not defined in the
- # locals nor globals scope)
- if not (name in node.frame() or name in node.root()):
- match name:
- case "exec":
- self.add_message("exec-used", node=node)
- case "reversed":
- self._check_reversed(node)
- case "eval":
- self.add_message("eval-used", node=node)
- @utils.only_required_for_messages("assert-on-tuple", "assert-on-string-literal")
- def visit_assert(self, node: nodes.Assert) -> None:
- """Check whether assert is used on a tuple or string literal."""
- match node.test:
- case nodes.Tuple(elts=elts) if len(elts) > 0:
- self.add_message("assert-on-tuple", node=node, confidence=HIGH)
- case nodes.Const(value=str() as val):
- when = "never" if val else "always"
- self.add_message("assert-on-string-literal", node=node, args=(when,))
- @utils.only_required_for_messages("duplicate-key")
- def visit_dict(self, node: nodes.Dict) -> None:
- """Check duplicate key in dictionary."""
- keys = set()
- for k, _ in node.items:
- match k:
- case nodes.Const():
- key = k.value
- case nodes.Attribute():
- key = k.as_string()
- case _:
- continue
- if key in keys:
- self.add_message("duplicate-key", node=node, args=key)
- keys.add(key)
- @utils.only_required_for_messages("duplicate-value")
- def visit_set(self, node: nodes.Set) -> None:
- """Check duplicate value in set."""
- values = set()
- for v in node.elts:
- if isinstance(v, nodes.Const):
- value = v.value
- else:
- continue
- if value in values:
- self.add_message(
- "duplicate-value", node=node, args=value, confidence=HIGH
- )
- values.add(value)
- def visit_try(self, node: nodes.Try) -> None:
- """Update try block flag."""
- self._trys.append(node)
- for final_node in node.finalbody:
- for return_node in final_node.nodes_of_class(nodes.Return):
- self.add_message("return-in-finally", node=return_node, confidence=HIGH)
- def leave_try(self, _: nodes.Try) -> None:
- """Update try block flag."""
- self._trys.pop()
- def _check_unreachable(
- self,
- node: nodes.Return | nodes.Continue | nodes.Break | nodes.Raise | nodes.Call,
- confidence: Confidence = HIGH,
- ) -> None:
- """Check unreachable code."""
- unreachable_statement = node.next_sibling()
- if unreachable_statement is not None:
- if (
- isinstance(node, nodes.Return)
- and isinstance(unreachable_statement, nodes.Expr)
- and isinstance(unreachable_statement.value, nodes.Yield)
- ):
- # Don't add 'unreachable' for empty generators.
- # Only add warning if 'yield' is followed by another node.
- unreachable_statement = unreachable_statement.next_sibling()
- if unreachable_statement is None:
- return
- self.add_message(
- "unreachable", node=unreachable_statement, confidence=confidence
- )
- def _check_not_in_finally(
- self,
- node: nodes.Break | nodes.Return,
- node_name: str,
- breaker_classes: tuple[nodes.NodeNG, ...] = (),
- ) -> None:
- """Check that a node is not inside a 'finally' clause of a
- 'try...finally' statement.
- If we find a parent which type is in breaker_classes before
- a 'try...finally' block we skip the whole check.
- """
- # if self._trys is empty, we're not an in try block
- if not self._trys:
- return
- # the node could be a grand-grand...-child of the 'try...finally'
- _parent = node.parent
- _node = node
- while _parent and not isinstance(_parent, breaker_classes):
- if hasattr(_parent, "finalbody") and _node in _parent.finalbody:
- self.add_message("lost-exception", node=node, args=node_name)
- return
- _node = _parent
- _parent = _node.parent
- def _check_reversed(self, node: nodes.Call) -> None:
- """Check that the argument to `reversed` is a sequence."""
- try:
- argument = utils.safe_infer(utils.get_argument_from_call(node, position=0))
- except utils.NoSuchArgumentError:
- pass
- else:
- match argument:
- case util.UninferableBase():
- return
- case None:
- # Nothing was inferred.
- # Try to see if we have iter().
- if isinstance(node.args[0], nodes.Call):
- try:
- func = next(node.args[0].func.infer())
- except astroid.InferenceError:
- return
- if getattr(
- func, "name", None
- ) == "iter" and utils.is_builtin_object(func):
- self.add_message("bad-reversed-sequence", node=node)
- return
- case nodes.List() | nodes.Tuple():
- return
- case astroid.Instance() if not self._py38_plus:
- # dicts are reversible, but only from Python 3.8 onward. Prior to
- # that, any class based on dict must explicitly provide a
- # __reversed__ method
- if any(
- ancestor.name == "dict" and utils.is_builtin_object(ancestor)
- for ancestor in itertools.chain(
- (argument._proxied,), argument._proxied.ancestors()
- )
- ):
- try:
- argument.locals[REVERSED_PROTOCOL_METHOD]
- except KeyError:
- self.add_message("bad-reversed-sequence", node=node)
- return
- if hasattr(argument, "getattr"):
- # everything else is not a proper sequence for reversed()
- for methods in REVERSED_METHODS:
- for meth in methods:
- try:
- argument.getattr(meth)
- except astroid.NotFoundError:
- break
- else:
- break
- else:
- self.add_message("bad-reversed-sequence", node=node)
- else:
- self.add_message("bad-reversed-sequence", node=node)
- @utils.only_required_for_messages("confusing-with-statement")
- def visit_with(self, node: nodes.With) -> None:
- # a "with" statement with multiple managers corresponds
- # to one AST "With" node with multiple items
- pairs = node.items
- if pairs:
- for prev_pair, pair in itertools.pairwise(pairs):
- if isinstance(prev_pair[1], nodes.AssignName) and (
- pair[1] is None and not isinstance(pair[0], nodes.Call)
- ):
- # Don't emit a message if the second is a function call
- # there's no way that can be mistaken for a name assignment.
- # If the line number doesn't match
- # we assume it's a nested "with".
- self.add_message("confusing-with-statement", node=node)
- def _check_self_assigning_variable(self, node: nodes.Assign) -> None:
- # Detect assigning to the same variable.
- scope = node.scope()
- scope_locals = scope.locals
- rhs_names = []
- targets = node.targets
- if isinstance(targets[0], nodes.Tuple):
- if len(targets) != 1:
- # A complex assignment, so bail out early.
- return
- targets = targets[0].elts
- if len(targets) == 1:
- # Unpacking a variable into the same name.
- return
- match node.value:
- case nodes.Name():
- if len(targets) != 1:
- return
- rhs_names = [node.value]
- case nodes.Tuple():
- rhs_count = len(node.value.elts)
- if len(targets) != rhs_count or rhs_count == 1:
- return
- rhs_names = node.value.elts
- for target, lhs_name in zip(targets, rhs_names):
- if not isinstance(lhs_name, nodes.Name):
- continue
- if not isinstance(target, nodes.AssignName):
- continue
- # Check that the scope is different from a class level, which is usually
- # a pattern to expose module level attributes as class level ones.
- if isinstance(scope, nodes.ClassDef) and target.name in scope_locals:
- continue
- if target.name == lhs_name.name:
- self.add_message(
- "self-assigning-variable", args=(target.name,), node=target
- )
- def _check_redeclared_assign_name(self, targets: list[nodes.NodeNG | None]) -> None:
- dummy_variables_rgx = self.linter.config.dummy_variables_rgx
- for target in targets:
- if not isinstance(target, nodes.Tuple):
- continue
- found_names = []
- for element in target.elts:
- if isinstance(element, nodes.Tuple):
- self._check_redeclared_assign_name([element])
- elif isinstance(element, nodes.AssignName) and element.name != "_":
- if dummy_variables_rgx and dummy_variables_rgx.match(element.name):
- return
- found_names.append(element.name)
- names = collections.Counter(found_names)
- for name, count in names.most_common():
- if count > 1:
- self.add_message(
- "redeclared-assigned-name", args=(name,), node=target
- )
- @utils.only_required_for_messages(
- "self-assigning-variable", "redeclared-assigned-name"
- )
- def visit_assign(self, node: nodes.Assign) -> None:
- self._check_self_assigning_variable(node)
- self._check_redeclared_assign_name(node.targets)
- @utils.only_required_for_messages("redeclared-assigned-name")
- def visit_for(self, node: nodes.For) -> None:
- self._check_redeclared_assign_name([node.target])
|