Mini Shell
# 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 Error checker from the basic checker."""
from __future__ import annotations
import itertools
from collections.abc import Iterator
from typing import Any
import astroid
from astroid import nodes
from astroid.typing import InferenceResult
from pylint.checkers import utils
from pylint.checkers.base.basic_checker import _BasicChecker
from pylint.checkers.utils import infer_all
from pylint.interfaces import HIGH
ABC_METACLASSES = {"_py_abc.ABCMeta", "abc.ABCMeta"} # Python 3.7+,
# List of methods which can be redefined
REDEFINABLE_METHODS = frozenset(("__module__",))
TYPING_FORWARD_REF_QNAME = "typing.ForwardRef"
def _get_break_loop_node(break_node: nodes.Break) -> nodes.For | nodes.While | None:
"""Returns the loop node that holds the break node in arguments.
Args:
break_node (astroid.Break): the break node of interest.
Returns:
astroid.For or astroid.While: the loop node holding the break node.
"""
loop_nodes = (nodes.For, nodes.While)
parent = break_node.parent
while not isinstance(parent, loop_nodes) or break_node in getattr(
parent, "orelse", []
):
break_node = parent
parent = parent.parent
if parent is None:
break
return parent
def _loop_exits_early(loop: nodes.For | nodes.While) -> bool:
"""Returns true if a loop may end with a break statement.
Args:
loop (astroid.For, astroid.While): the loop node inspected.
Returns:
bool: True if the loop may end with a break statement, False otherwise.
"""
loop_nodes = (nodes.For, nodes.While)
definition_nodes = (nodes.FunctionDef, nodes.ClassDef)
inner_loop_nodes: list[nodes.For | nodes.While] = [
_node
for _node in loop.nodes_of_class(loop_nodes, skip_klass=definition_nodes)
if _node != loop
]
return any(
_node
for _node in loop.nodes_of_class(nodes.Break, skip_klass=definition_nodes)
if _get_break_loop_node(_node) not in inner_loop_nodes
)
def _has_abstract_methods(node: nodes.ClassDef) -> bool:
"""Determine if the given `node` has abstract methods.
The methods should be made abstract by decorating them
with `abc` decorators.
"""
return len(utils.unimplemented_abstract_methods(node)) > 0
def redefined_by_decorator(node: nodes.FunctionDef) -> bool:
"""Return True if the object is a method redefined via decorator.
For example:
@property
def x(self): return self._x
@x.setter
def x(self, value): self._x = value
"""
if node.decorators:
for decorator in node.decorators.nodes:
if (
isinstance(decorator, nodes.Attribute)
and getattr(decorator.expr, "name", None) == node.name
):
return True
return False
class BasicErrorChecker(_BasicChecker):
msgs = {
"E0100": (
"__init__ method is a generator",
"init-is-generator",
"Used when the special class method __init__ is turned into a "
"generator by a yield in its body.",
),
"E0101": (
"Explicit return in __init__",
"return-in-init",
"Used when the special class method __init__ has an explicit "
"return value.",
),
"E0102": (
"%s already defined line %s",
"function-redefined",
"Used when a function / class / method is redefined.",
),
"E0103": (
"%r not properly in loop",
"not-in-loop",
"Used when break or continue keywords are used outside a loop.",
),
"E0104": (
"Return outside function",
"return-outside-function",
'Used when a "return" statement is found outside a function or method.',
),
"E0105": (
"Yield outside function",
"yield-outside-function",
'Used when a "yield" statement is found outside a function or method.',
),
"E0106": (
"Return with argument inside generator",
"return-arg-in-generator",
'Used when a "return" statement with an argument is found '
"outside in a generator function or method (e.g. with some "
'"yield" statements).',
{"maxversion": (3, 3)},
),
"E0107": (
"Use of the non-existent %s operator",
"nonexistent-operator",
"Used when you attempt to use the C-style pre-increment or "
"pre-decrement operator -- and ++, which doesn't exist in Python.",
),
"E0108": (
"Duplicate argument name %s in function definition",
"duplicate-argument-name",
"Duplicate argument names in function definitions are syntax errors.",
),
"E0110": (
"Abstract class %r with abstract methods instantiated",
"abstract-class-instantiated",
"Used when an abstract class with `abc.ABCMeta` as metaclass "
"has abstract methods and is instantiated.",
),
"W0120": (
"Else clause on loop without a break statement, remove the else and"
" de-indent all the code inside it",
"useless-else-on-loop",
"Loops should only have an else clause if they can exit early "
"with a break statement, otherwise the statements under else "
"should be on the same scope as the loop itself.",
),
"E0112": (
"More than one starred expression in assignment",
"too-many-star-expressions",
"Emitted when there are more than one starred "
"expressions (`*x`) in an assignment. This is a SyntaxError.",
),
"E0113": (
"Starred assignment target must be in a list or tuple",
"invalid-star-assignment-target",
"Emitted when a star expression is used as a starred assignment target.",
),
"E0114": (
"Can use starred expression only in assignment target",
"star-needs-assignment-target",
"Emitted when a star expression is not used in an assignment target.",
),
"E0115": (
"Name %r is nonlocal and global",
"nonlocal-and-global",
"Emitted when a name is both nonlocal and global.",
),
"E0116": (
"'continue' not supported inside 'finally' clause",
"continue-in-finally",
"Emitted when the `continue` keyword is found "
"inside a finally clause, which is a SyntaxError.",
),
"E0117": (
"nonlocal name %s found without binding",
"nonlocal-without-binding",
"Emitted when a nonlocal variable does not have an attached "
"name somewhere in the parent scopes",
),
"E0118": (
"Name %r is used prior to global declaration",
"used-prior-global-declaration",
"Emitted when a name is used prior a global declaration, "
"which results in an error since Python 3.6.",
{"minversion": (3, 6)},
),
}
def open(self) -> None:
py_version = self.linter.config.py_version
self._py38_plus = py_version >= (3, 8)
@utils.only_required_for_messages("function-redefined")
def visit_classdef(self, node: nodes.ClassDef) -> None:
self._check_redefinition("class", node)
def _too_many_starred_for_tuple(self, assign_tuple: nodes.Tuple) -> bool:
starred_count = 0
for elem in assign_tuple.itered():
if isinstance(elem, nodes.Tuple):
return self._too_many_starred_for_tuple(elem)
if isinstance(elem, nodes.Starred):
starred_count += 1
return starred_count > 1
@utils.only_required_for_messages(
"too-many-star-expressions", "invalid-star-assignment-target"
)
def visit_assign(self, node: nodes.Assign) -> None:
# Check *a, *b = ...
assign_target = node.targets[0]
# Check *a = b
if isinstance(node.targets[0], nodes.Starred):
self.add_message("invalid-star-assignment-target", node=node)
if not isinstance(assign_target, nodes.Tuple):
return
if self._too_many_starred_for_tuple(assign_target):
self.add_message("too-many-star-expressions", node=node)
@utils.only_required_for_messages("star-needs-assignment-target")
def visit_starred(self, node: nodes.Starred) -> None:
"""Check that a Starred expression is used in an assignment target."""
if isinstance(node.parent, nodes.Call):
# f(*args) is converted to Call(args=[Starred]), so ignore
# them for this check.
return
if isinstance(node.parent, (nodes.List, nodes.Tuple, nodes.Set, nodes.Dict)):
# PEP 448 unpacking.
return
stmt = node.statement(future=True)
if not isinstance(stmt, nodes.Assign):
return
if stmt.value is node or stmt.value.parent_of(node):
self.add_message("star-needs-assignment-target", node=node)
@utils.only_required_for_messages(
"init-is-generator",
"return-in-init",
"function-redefined",
"return-arg-in-generator",
"duplicate-argument-name",
"nonlocal-and-global",
"used-prior-global-declaration",
)
def visit_functiondef(self, node: nodes.FunctionDef) -> None:
self._check_nonlocal_and_global(node)
self._check_name_used_prior_global(node)
if not redefined_by_decorator(
node
) and not utils.is_registered_in_singledispatch_function(node):
self._check_redefinition(node.is_method() and "method" or "function", node)
# checks for max returns, branch, return in __init__
returns = node.nodes_of_class(
nodes.Return, skip_klass=(nodes.FunctionDef, nodes.ClassDef)
)
if node.is_method() and node.name == "__init__":
if node.is_generator():
self.add_message("init-is-generator", node=node)
else:
values = [r.value for r in returns]
# Are we returning anything but None from constructors
if any(v for v in values if not utils.is_none(v)):
self.add_message("return-in-init", node=node)
# Check for duplicate names by clustering args with same name for detailed report
arg_clusters = {}
arguments: Iterator[Any] = filter(None, [node.args.args, node.args.kwonlyargs])
for arg in itertools.chain.from_iterable(arguments):
if arg.name in arg_clusters:
self.add_message(
"duplicate-argument-name",
node=arg,
args=(arg.name,),
confidence=HIGH,
)
else:
arg_clusters[arg.name] = arg
visit_asyncfunctiondef = visit_functiondef
def _check_name_used_prior_global(self, node: nodes.FunctionDef) -> None:
scope_globals = {
name: child
for child in node.nodes_of_class(nodes.Global)
for name in child.names
if child.scope() is node
}
if not scope_globals:
return
for node_name in node.nodes_of_class(nodes.Name):
if node_name.scope() is not node:
continue
name = node_name.name
corresponding_global = scope_globals.get(name)
if not corresponding_global:
continue
global_lineno = corresponding_global.fromlineno
if global_lineno and global_lineno > node_name.fromlineno:
self.add_message(
"used-prior-global-declaration", node=node_name, args=(name,)
)
def _check_nonlocal_and_global(self, node: nodes.FunctionDef) -> None:
"""Check that a name is both nonlocal and global."""
def same_scope(current: nodes.Global | nodes.Nonlocal) -> bool:
return current.scope() is node
from_iter = itertools.chain.from_iterable
nonlocals = set(
from_iter(
child.names
for child in node.nodes_of_class(nodes.Nonlocal)
if same_scope(child)
)
)
if not nonlocals:
return
global_vars = set(
from_iter(
child.names
for child in node.nodes_of_class(nodes.Global)
if same_scope(child)
)
)
for name in nonlocals.intersection(global_vars):
self.add_message("nonlocal-and-global", args=(name,), node=node)
@utils.only_required_for_messages("return-outside-function")
def visit_return(self, node: nodes.Return) -> None:
if not isinstance(node.frame(future=True), nodes.FunctionDef):
self.add_message("return-outside-function", node=node)
@utils.only_required_for_messages("yield-outside-function")
def visit_yield(self, node: nodes.Yield) -> None:
self._check_yield_outside_func(node)
@utils.only_required_for_messages("yield-outside-function")
def visit_yieldfrom(self, node: nodes.YieldFrom) -> None:
self._check_yield_outside_func(node)
@utils.only_required_for_messages("not-in-loop", "continue-in-finally")
def visit_continue(self, node: nodes.Continue) -> None:
self._check_in_loop(node, "continue")
@utils.only_required_for_messages("not-in-loop")
def visit_break(self, node: nodes.Break) -> None:
self._check_in_loop(node, "break")
@utils.only_required_for_messages("useless-else-on-loop")
def visit_for(self, node: nodes.For) -> None:
self._check_else_on_loop(node)
@utils.only_required_for_messages("useless-else-on-loop")
def visit_while(self, node: nodes.While) -> None:
self._check_else_on_loop(node)
@utils.only_required_for_messages("nonexistent-operator")
def visit_unaryop(self, node: nodes.UnaryOp) -> None:
"""Check use of the non-existent ++ and -- operators."""
if (
(node.op in "+-")
and isinstance(node.operand, nodes.UnaryOp)
and (node.operand.op == node.op)
and (node.col_offset + 1 == node.operand.col_offset)
):
self.add_message("nonexistent-operator", node=node, args=node.op * 2)
def _check_nonlocal_without_binding(self, node: nodes.Nonlocal, name: str) -> None:
current_scope = node.scope()
while current_scope.parent is not None:
if not isinstance(current_scope, (nodes.ClassDef, nodes.FunctionDef)):
self.add_message("nonlocal-without-binding", args=(name,), node=node)
return
# Search for `name` in the parent scope if:
# `current_scope` is the same scope in which the `nonlocal` name is declared
# or `name` is not in `current_scope.locals`.
if current_scope is node.scope() or name not in current_scope.locals:
current_scope = current_scope.parent.scope()
continue
# Okay, found it.
return
if not isinstance(current_scope, nodes.FunctionDef):
self.add_message(
"nonlocal-without-binding", args=(name,), node=node, confidence=HIGH
)
@utils.only_required_for_messages("nonlocal-without-binding")
def visit_nonlocal(self, node: nodes.Nonlocal) -> None:
for name in node.names:
self._check_nonlocal_without_binding(node, name)
@utils.only_required_for_messages("abstract-class-instantiated")
def visit_call(self, node: nodes.Call) -> None:
"""Check instantiating abstract class with
abc.ABCMeta as metaclass.
"""
for inferred in infer_all(node.func):
self._check_inferred_class_is_abstract(inferred, node)
def _check_inferred_class_is_abstract(
self, inferred: InferenceResult, node: nodes.Call
) -> None:
if not isinstance(inferred, nodes.ClassDef):
return
klass = utils.node_frame_class(node)
if klass is inferred:
# Don't emit the warning if the class is instantiated
# in its own body or if the call is not an instance
# creation. If the class is instantiated into its own
# body, we're expecting that it knows what it is doing.
return
# __init__ was called
abstract_methods = _has_abstract_methods(inferred)
if not abstract_methods:
return
metaclass = inferred.metaclass()
if metaclass is None:
# Python 3.4 has `abc.ABC`, which won't be detected
# by ClassNode.metaclass()
for ancestor in inferred.ancestors():
if ancestor.qname() == "abc.ABC":
self.add_message(
"abstract-class-instantiated", args=(inferred.name,), node=node
)
break
return
if metaclass.qname() in ABC_METACLASSES:
self.add_message(
"abstract-class-instantiated", args=(inferred.name,), node=node
)
def _check_yield_outside_func(self, node: nodes.Yield) -> None:
if not isinstance(node.frame(future=True), (nodes.FunctionDef, nodes.Lambda)):
self.add_message("yield-outside-function", node=node)
def _check_else_on_loop(self, node: nodes.For | nodes.While) -> None:
"""Check that any loop with an else clause has a break statement."""
if node.orelse and not _loop_exits_early(node):
self.add_message(
"useless-else-on-loop",
node=node,
# This is not optimal, but the line previous
# to the first statement in the else clause
# will usually be the one that contains the else:.
line=node.orelse[0].lineno - 1,
)
def _check_in_loop(
self, node: nodes.Continue | nodes.Break, node_name: str
) -> None:
"""Check that a node is inside a for or while loop."""
for parent in node.node_ancestors():
if isinstance(parent, (nodes.For, nodes.While)):
if node not in parent.orelse:
return
if isinstance(parent, (nodes.ClassDef, nodes.FunctionDef)):
break
if (
isinstance(parent, nodes.TryFinally)
and node in parent.finalbody
and isinstance(node, nodes.Continue)
and not self._py38_plus
):
self.add_message("continue-in-finally", node=node)
self.add_message("not-in-loop", node=node, args=node_name)
def _check_redefinition(
self, redeftype: str, node: nodes.Call | nodes.FunctionDef
) -> None:
"""Check for redefinition of a function / method / class name."""
parent_frame = node.parent.frame(future=True)
# Ignore function stubs created for type information
redefinitions = [
i
for i in parent_frame.locals[node.name]
if not (isinstance(i.parent, nodes.AnnAssign) and i.parent.simple)
]
defined_self = next(
(local for local in redefinitions if not utils.is_overload_stub(local)),
node,
)
if defined_self is not node and not astroid.are_exclusive(node, defined_self):
# Additional checks for methods which are not considered
# redefined, since they are already part of the base API.
if (
isinstance(parent_frame, nodes.ClassDef)
and node.name in REDEFINABLE_METHODS
):
return
# Skip typing.overload() functions.
if utils.is_overload_stub(node):
return
# Exempt functions redefined on a condition.
if isinstance(node.parent, nodes.If):
# Exempt "if not <func>" cases
if (
isinstance(node.parent.test, nodes.UnaryOp)
and node.parent.test.op == "not"
and isinstance(node.parent.test.operand, nodes.Name)
and node.parent.test.operand.name == node.name
):
return
# Exempt "if <func> is not None" cases
# pylint: disable=too-many-boolean-expressions
if (
isinstance(node.parent.test, nodes.Compare)
and isinstance(node.parent.test.left, nodes.Name)
and node.parent.test.left.name == node.name
and node.parent.test.ops[0][0] == "is"
and isinstance(node.parent.test.ops[0][1], nodes.Const)
and node.parent.test.ops[0][1].value is None
):
return
# Check if we have forward references for this node.
try:
redefinition_index = redefinitions.index(node)
except ValueError:
pass
else:
for redefinition in redefinitions[:redefinition_index]:
inferred = utils.safe_infer(redefinition)
if (
inferred
and isinstance(inferred, astroid.Instance)
and inferred.qname() == TYPING_FORWARD_REF_QNAME
):
return
dummy_variables_rgx = self.linter.config.dummy_variables_rgx
if dummy_variables_rgx and dummy_variables_rgx.match(node.name):
return
self.add_message(
"function-redefined",
node=node,
args=(redeftype, defined_self.fromlineno),
)
Zerion Mini Shell 1.0