<filename>mr_proper/pure_validators.py
import ast
from typing import List, Optional, Callable, cast

from mr_proper.common_types import AnyFuncdef
from mr_proper.config import (
    CALLABLES_BLACKLIST, ATTRIBUTES_BLACKLIST, FORBIDDEN_ARGUMENT_TYPES,
    FORBIDDEN_ATTR_CALLS_FOR_ARGS)
from mr_proper.utils.ast import (
    get_nodes_from_funcdef_body, is_imported_from_stdlib,
    get_local_var_names_from_funcdef,
    BUILTINS_LIST)
from mr_proper.utils.python_naming import is_python_class_name


def has_no_blacklisted_calls(
    funcdef_node: AnyFuncdef,
    file_ast_tree: Optional[ast.Module],
) -> List[str]:
    errors: List[str] = []
    for call_node in [n for n in ast.walk(funcdef_node) if isinstance(n, ast.Call)]:
        if isinstance(call_node.func, ast.Name) and call_node.func.id in CALLABLES_BLACKLIST:
            errors.append(f'it calls forbidden function ({call_node.func.id})')
    for attr_node in [n for n in ast.walk(funcdef_node) if isinstance(n, ast.Attribute)]:
        if attr_node.attr in ATTRIBUTES_BLACKLIST:
            errors.append(f'it accesses forbidden attribute ({attr_node.attr})')
    return errors


def uses_only_args_and_local_vars(
    funcdef_node: AnyFuncdef,
    file_ast_tree: Optional[ast.Module],
    allow_external_class_usage: bool = False,
    allow_stdlib_usage: bool = True,
) -> List[str]:
    all_names = [n.id for n in get_nodes_from_funcdef_body(funcdef_node, [ast.Name])]
    args_names = [a.arg for a in funcdef_node.args.args]
    called_names = [
        c.func.id
        for c in get_nodes_from_funcdef_body(funcdef_node, [ast.Call])
        if isinstance(c.func, ast.Name)
    ]
    local_vars_names = get_local_var_names_from_funcdef(funcdef_node)
    subfunctions_arguments = {
        a.arg
        for n in get_nodes_from_funcdef_body(funcdef_node, [ast.FunctionDef, ast.AsyncFunctionDef])
        for a in n.args.args  # type: ignore
    }

    nonlocal_names = list(
        set(all_names)
        - set(args_names)
        - set(local_vars_names)
        - set(called_names)
        - subfunctions_arguments
        - BUILTINS_LIST,
    )
    if allow_external_class_usage:
        nonlocal_names = [n for n in nonlocal_names if not is_python_class_name(n)]
    if allow_stdlib_usage and file_ast_tree:
        nonlocal_names = [
            n for n in nonlocal_names
            if not is_imported_from_stdlib(n, file_ast_tree)
        ]

    return [f'it uses external name ({n})' for n in nonlocal_names]


def has_returns(funcdef_node: AnyFuncdef, file_ast_tree: Optional[ast.Module]) -> List[str]:
    if not get_nodes_from_funcdef_body(funcdef_node, [ast.Return, ast.Yield]):
        return ['it has no returns']
    return []


def not_mutates_args(funcdef_node: AnyFuncdef, file_ast_tree: Optional[ast.Module]) -> List[str]:
    args_names = {a.arg for a in funcdef_node.args.args}
    mutating_actions = [  # noqa
        (  # n +=
            ast.AugAssign,
            lambda n: True,
            lambda n: n.target,
        ),
        (  # n[...] =
            ast.Assign,
            lambda n: (
                n.targets
                and isinstance(n.targets[0], ast.Subscript)
                and isinstance(n.targets[0].value, ast.Name)
                and n.targets[0].value.id in args_names
            ),
            lambda n: n.targets[0],
        ),
        (  # n.foo =
            ast.Assign,
            lambda n: n.targets and isinstance(n.targets[0], ast.Attribute),
            lambda n: n.targets[0].value,
        ),
        (  # n.append/remove/...(...)
            ast.Attribute,
            lambda n: (
                n.attr in FORBIDDEN_ATTR_CALLS_FOR_ARGS
                and isinstance(n.value, ast.Name)
                and n.value.id in args_names
            ),
            lambda n: n.value,
        ),
    ]

    errors: List[str] = []
    for node_type, node_validator, node_extractor in mutating_actions:
        node_validator = cast(Callable[[ast.AST], bool], node_validator)
        node_extractor = cast(Callable[[ast.AST], ast.AST], node_extractor)
        for node in get_nodes_from_funcdef_body(funcdef_node, [node_type]):
            if not node_validator(node):
                continue
            for name_node in [n for n in ast.walk(node_extractor(node)) if isinstance(n, ast.Name)]:
                if name_node.id in args_names:
                    errors.append(f'it mutates its own argument (on line {node.lineno})')
    return errors


def not_has_local_imports(
    funcdef_node: AnyFuncdef,
    file_ast_tree: Optional[ast.Module],
) -> List[str]:
    if get_nodes_from_funcdef_body(funcdef_node, [ast.ImportFrom, ast.Import]):
        return ['it has local imports']
    return []


def not_has_forbidden_arguments_types(
    funcdef_node: AnyFuncdef,
    file_ast_tree: Optional[ast.Module],
    extra_forbidden_argument_type_names: List[str] = None,
) -> List[str]:
    errors: List[str] = []
    forbidden_argument_type_names = FORBIDDEN_ARGUMENT_TYPES
    if extra_forbidden_argument_type_names:
        forbidden_argument_type_names += extra_forbidden_argument_type_names
    for argument in funcdef_node.args.args:
        for annotation_part_node in ast.walk(argument.annotation) if argument.annotation else []:
            if not isinstance(annotation_part_node, ast.Name):
                continue
            type_without_prefix = annotation_part_node.id.split('.')[-1]
            if type_without_prefix in forbidden_argument_type_names:
                errors.append(f'it has type {type_without_prefix} in argument types')
    return errors


def not_uses_self_or_class_vars(
    funcdef_node: AnyFuncdef,
    file_ast_tree: Optional[ast.Module],
) -> List[str]:
    forbidden_names = {'self', 'cls', 'super'}

    errors: List[str] = []
    for name_node in get_nodes_from_funcdef_body(funcdef_node, [ast.Name]):
        if name_node.id in forbidden_names:
            errors.append(f'it uses {name_node.id} var')
    return errors

<filename>mr_proper/pure_validators.py
import ast
from typing import List, Optional, Callable, cast

from mr_proper.common_types import AnyFuncdef
from mr_proper.config import (
    CALLABLES_BLACKLIST, ATTRIBUTES_BLACKLIST, FORBIDDEN_ARGUMENT_TYPES,
    FORBIDDEN_ATTR_CALLS_FOR_ARGS)
from mr_proper.utils.ast import (
    get_nodes_from_funcdef_body, is_imported_from_stdlib,
    get_local_var_names_from_funcdef,
    BUILTINS_LIST)
from mr_proper.utils.python_naming import is_python_class_name


def has_no_blacklisted_calls(
    funcdef_node: AnyFuncdef,
    file_ast_tree: Optional[ast.Module],
) -> List[str]:
    errors: List[str] = []
    for call_node in [n for n in ast.walk(funcdef_node) if isinstance(n, ast.Call)]:
        if isinstance(call_node.func, ast.Name) and call_node.func.id in CALLABLES_BLACKLIST:
            errors.append(f'it calls forbidden function ({call_node.func.id})')
    for attr_node in [n for n in ast.walk(funcdef_node) if isinstance(n, ast.Attribute)]:
        if attr_node.attr in ATTRIBUTES_BLACKLIST:
            errors.append(f'it accesses forbidden attribute ({attr_node.attr})')
    return errors


def uses_only_args_and_local_vars(
    funcdef_node: AnyFuncdef,
    file_ast_tree: Optional[ast.Module],
    allow_external_class_usage: bool = False,
    allow_stdlib_usage: bool = True,
) -> List[str]:
    all_names = [n.id for n in get_nodes_from_funcdef_body(funcdef_node, [ast.Name])]
    args_names = [a.arg for a in funcdef_node.args.args]
    called_names = [
        c.func.id
        for c in get_nodes_from_funcdef_body(funcdef_node, [ast.Call])
        if isinstance(c.func, ast.Name)
    ]
    local_vars_names = get_local_var_names_from_funcdef(funcdef_node)
    subfunctions_arguments = {
        a.arg
        for n in get_nodes_from_funcdef_body(funcdef_node, [ast.FunctionDef, ast.AsyncFunctionDef])
        for a in n.args.args  # type: ignore
    }

    nonlocal_names = list(
        set(all_names)
        - set(args_names)
        - set(local_vars_names)
        - set(called_names)
        - subfunctions_arguments
        - BUILTINS_LIST,
    )
    if allow_external_class_usage:
        nonlocal_names = [n for n in nonlocal_names if not is_python_class_name(n)]
    if allow_stdlib_usage and file_ast_tree:
        nonlocal_names = [
            n for n in nonlocal_names
            if not is_imported_from_stdlib(n, file_ast_tree)
        ]

    return [f'it uses external name ({n})' for n in nonlocal_names]


def has_returns(funcdef_node: AnyFuncdef, file_ast_tree: Optional[ast.Module]) -> List[str]:
    if not get_nodes_from_funcdef_body(funcdef_node, [ast.Return, ast.Yield]):
        return ['it has no returns']
    return []


def not_mutates_args(funcdef_node: AnyFuncdef, file_ast_tree: Optional[ast.Module]) -> List[str]:
    args_names = {a.arg for a in funcdef_node.args.args}
    mutating_actions = [  # noqa
        (  # n +=
            ast.AugAssign,
            lambda n: True,
            lambda n: n.target,
        ),
        (  # n[...] =
            ast.Assign,
            lambda n: (
                n.targets
                and isinstance(n.targets[0], ast.Subscript)
                and isinstance(n.targets[0].value, ast.Name)
                and n.targets[0].value.id in args_names
            ),
            lambda n: n.targets[0],
        ),
        (  # n.foo =
            ast.Assign,
            lambda n: n.targets and isinstance(n.targets[0], ast.Attribute),
            lambda n: n.targets[0].value,
        ),
        (  # n.append/remove/...(...)
            ast.Attribute,
            lambda n: (
                n.attr in FORBIDDEN_ATTR_CALLS_FOR_ARGS
                and isinstance(n.value, ast.Name)
                and n.value.id in args_names
            ),
            lambda n: n.value,
        ),
    ]

    errors: List[str] = []
    for node_type, node_validator, node_extractor in mutating_actions:
        node_validator = cast(Callable[[ast.AST], bool], node_validator)
        node_extractor = cast(Callable[[ast.AST], ast.AST], node_extractor)
        for node in get_nodes_from_funcdef_body(funcdef_node, [node_type]):
            if not node_validator(node):
                continue
            for name_node in [n for n in ast.walk(node_extractor(node)) if isinstance(n, ast.Name)]:
                if name_node.id in args_names:
                    errors.append(f'it mutates its own argument (on line {node.lineno})')
    return errors


def not_has_local_imports(
    funcdef_node: AnyFuncdef,
    file_ast_tree: Optional[ast.Module],
) -> List[str]:
    if get_nodes_from_funcdef_body(funcdef_node, [ast.ImportFrom, ast.Import]):
        return ['it has local imports']
    return []


def not_has_forbidden_arguments_types(
    funcdef_node: AnyFuncdef,
    file_ast_tree: Optional[ast.Module],
    extra_forbidden_argument_type_names: List[str] = None,
) -> List[str]:
    errors: List[str] = []
    forbidden_argument_type_names = FORBIDDEN_ARGUMENT_TYPES
    if extra_forbidden_argument_type_names:
        forbidden_argument_type_names += extra_forbidden_argument_type_names
    for argument in funcdef_node.args.args:
        for annotation_part_node in ast.walk(argument.annotation) if argument.annotation else []:
            if not isinstance(annotation_part_node, ast.Name):
                continue
            type_without_prefix = annotation_part_node.id.split('.')[-1]
            if type_without_prefix in forbidden_argument_type_names:
                errors.append(f'it has type {type_without_prefix} in argument types')
    return errors


def not_uses_self_or_class_vars(
    funcdef_node: AnyFuncdef,
    file_ast_tree: Optional[ast.Module],
) -> List[str]:
    forbidden_names = {'self', 'cls', 'super'}

    errors: List[str] = []
    for name_node in get_nodes_from_funcdef_body(funcdef_node, [ast.Name]):
        if name_node.id in forbidden_names:
            errors.append(f'it uses {name_node.id} var')
    return errors

# type: ignore # noqa # n += # n[...] = # n.foo = # n.append/remove/...(...)