#!/usr/bin/env python3
# --------------------( LICENSE                            )--------------------
# Copyright (c) 2014-2025 Beartype authors.
# See "LICENSE" for further details.

'''
Beartype **abstract syntax tree (AST) transformers** (i.e., low-level classes
instrumenting well-typed third-party modules with runtime type-checking
dynamically generated by the :func:`beartype.beartype` decorator).

This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS                            }....................
from ast import (
    ClassDef,
    NodeTransformer,
)
from beartype.claw._ast._scope.clawastscope import BeartypeNodeScope
from beartype.claw._ast._scope.clawastscopes import BeartypeNodeScopes
from beartype.claw._ast._kind.clawastassign import (
    BeartypeNodeTransformerAssignMixin)
from beartype.claw._ast._kind.clawastimport import (
    BeartypeNodeTransformerImportMixin)
from beartype.claw._ast._kind.clawastmodule import (
    BeartypeNodeTransformerModuleMixin)
from beartype.claw._ast._pep.clawastpep695 import (
    BeartypeNodeTransformerPep695Mixin)
from beartype.claw._ast._clawastutil import BeartypeNodeTransformerUtilityMixin
from beartype.roar._roarexc import _BeartypeClawAstNodeScopesException
from beartype.typing import (
    Optional,
)
from beartype._conf.confmain import BeartypeConf
from beartype._data.api.standard.dataast import TYPES_NODE_LEXICAL_SCOPE
from beartype._data.typing.datatyping import (
    ListStrs,
    NodeCallable,
    NodeT,
)
from beartype._util.ast.utilasttest import is_node_callable_typed
from beartype._util.cache.utilcachecall import property_cached

# ....................{ SUBCLASSES                         }....................
#FIXME: Unit test us up, please.
class BeartypeNodeTransformer(
    # PEP-agnostic superclass defining "core" AST node transformation logic.
    NodeTransformer,

    # PEP-agnostic mixins defining supplementary AST node functionality in a
    # PEP-agnostic manner.
    BeartypeNodeTransformerAssignMixin,
    BeartypeNodeTransformerImportMixin,
    BeartypeNodeTransformerModuleMixin,
    BeartypeNodeTransformerUtilityMixin,

    # PEP-specific mixins defining additional AST node transformations in a
    # PEP-specific manner.
    BeartypeNodeTransformerPep695Mixin,
):
    '''
    **Beartype abstract syntax tree (AST) node transformer** (i.e., visitor
    pattern recursively transforming the AST tree passed to the :meth:`visit`
    method by decorating all typed callables and classes by the
    :func:`beartype.beartype` decorator).

    Design
    ------
    This class was largely designed by reverse-engineering the standard
    :mod:`ast` module using the following code snippet. When run as the body of
    a script from the command line (e.g., ``python3 {muh_script}.py``), this
    snippet pretty-prints the desired target AST subtree implementing the
    desired source code (specified in this snippet via the ``CODE`` global). In
    short, this snippet trivializes the definition of arbitrarily complex
    AST-based code from arbitrarily complex Python code:

    .. code-block:: python

       import ast

       # Arbitrary desired code to pretty-print the AST representation of.
       CODE = """
       from beartype import beartype
       from beartype._conf.confcache import beartype_conf_id_to_conf

       @beartype(conf=beartype_conf_id_to_conf[139870142111616])
       def muh_func(): pass
       """

       # Dismantled, this is:
       # * "indent=...", producing pretty-printed (i.e., indented) output.
       # * "include_attributes=True", enabling pseudo-nodes (i.e., nodes lacking
       #   associated code metadata) to be distinguished from standard nodes
       #   (i.e., nodes having such metadata).
       print(ast.dump(ast.parse(CODE), indent=4, include_attributes=True))

    Attributes
    ----------
    _conf : BeartypeConf
        **Beartype configuration** (i.e., dataclass configuring the
        :mod:`beartype.beartype` decorator for *all* decoratable objects
        recursively decorated by this node transformer).
    _module_name : str
        Fully-qualified name of the current module being transformed.
    _scopes : BeartypeNodeScopes
        **Lexical scope stack** (i.e., list of the one or more dataclasses
        aggregating all metadata required to detect and manage lexical scopes
        being recursively visited by this AST transformer).

    See Also
    --------
    https://github.com/agronholm/typeguard/blob/fe5b578595e00d5e45c3795d451dcd7935743809/src/typeguard/importhook.py
        Last commit of the third-party Python package whose
        ``@typeguard.importhook.TypeguardTransformer`` class implements import
        hooks performing runtime type-checking in a similar manner, strongly
        inspiring this implementation.

        Note that all subsequent commits to that package generalize those import
        hooks into something else entirely, which increasingly resembles a
        static type-checker run at runtime; while fascinating and almost
        certainly ingenious, those commits are sufficiently inscrutable,
        undocumented, and unintelligible to warrant caution. Nonetheless, thanks
        so much to @agronholm (Alex Grönholm) for his pulse-pounding innovations
        in this burgeoning field! Our AST transformer is for you, @agronholm.
    '''

    # ..................{ CLASS VARIABLES                    }..................
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    # CAUTION: Subclasses declaring uniquely subclass-specific instance
    # variables *MUST* additionally slot those variables. Subclasses violating
    # this constraint will be usable but unslotted, which defeats the purpose.
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    # Slot all instance variables defined on this object to reduce the costs of
    # both reading and writing these variables by approximately ~10%.
    __slots__ = (
        '_conf',
        '_module_name',
        '_scopes',
    )

    # ..................{ INITIALIZERS                       }..................
    def __init__(
        self,

        # Mandatory keyword-only parameters.
        *,
        module_name: str,
        conf: BeartypeConf,
    ) -> None:
        '''
        Initialize this node transformer.

        Parameters
        ----------
        module_name : str
            Fully-qualified name of the external third-party module being
            transformed by this node transformer.
        conf : BeartypeConf
            **Beartype configuration** (i.e., dataclass configuring the
            :mod:`beartype.beartype` decorator for *all* decoratable objects
            recursively decorated by this node transformer).
        '''
        assert isinstance(module_name, str), (
            f'{repr(module_name)} not string.')
        assert isinstance(conf, BeartypeConf), (
            f'{repr(conf)} not beartype configuration.')

        # Initialize our superclass.
        # print(f'Initializing module "{module_name}" AST transformation...')
        super().__init__()

        # Classify all passed parameters.
        self._conf = conf
        self._module_name = module_name

        # Lexical scope stack, initially containing *ONLY* the default global
        # scope for statements in the body of the current module.
        self._scopes = BeartypeNodeScopes(module_name=module_name)

    # ..................{ PROPERTIES                         }..................
    #FIXME: Unit test us up, please. *sigh*
    @property  # type: ignore[misc]
    @property_cached
    def _module_basenames(self) -> ListStrs:
        '''
        List of the one or more unqualified basenames comprising the
        fully-qualified ``"."``-delimited name of the currently visited module.

        This property is memoized for efficiency.

        Returns
        -------
        list[str]
            List of the one or more unqualified basenames comprising the
            fully-qualified name of the currently visited module.
        '''

        # List of each unqualified basename comprising the name of the currently
        # visited module, split from the fully-qualified name of that module on
        # "." delimiters.
        #
        # Note that the "str.split('.')" and "str.rsplit('.')" calls produce the
        # same lists under all edge cases. We arbitrarily call the former rather
        # than the latter for simplicity.
        module_basenames = self._module_name.split('.')

        # Return this list.
        return module_basenames


    #FIXME: Unit test us up, please. *sigh*
    @property
    def _scope(self) -> BeartypeNodeScope:  # type: ignore[override]
        '''
        **Currently visited lexical scope** (i.e., dataclass aggregating all
        metadata required to detect and manage the lexical scope of the
        currently visited node of the currently visited module) if this
        abstract syntax tree (AST) transformer has already visited at least that
        module's root :class:`ast.Module` node via the :meth:`.visitModule`
        method *or* raise an exception otherwise (i.e., if this property is
        unsafely accessed at an early time).

        Returns
        -------
        BeartypeNodeScope
            :data:`True` only if the current lexical scope is a module scope.

        Raises
        ------
        _BeartypeClawAstNodeScopesException
            If this property is unsafely accessed at an early time.
        '''

        # If the lexical scope stack is empty, this AST transformer has yet to
        # visit the root "Module" node of the currently visited module. In this
        # case, raise an exception.
        if not self._scopes:
            raise _BeartypeClawAstNodeScopesException(
                f'Module "{self._module_name}" '
                f'global lexical scope has yet to be visited '
                f'(i.e., visitModule() method not previously called).'
            )
        # Else, the lexical scope stack is non-empty.

        # Return the last lexical scope on this stack -- the top-most item
        # signifying the currently visited lexical scope.
        return self._scopes[-1]

    # ..................{ SUPERCLASS                         }..................
    # Overridden methods first defined by the "NodeTransformer" superclass.
    def generic_visit(self, node: NodeT) -> NodeT:
        '''
        Recursively visit and possibly transform *all* child nodes of the passed
        parent node in-place (i.e., preserving this parent node as is).

        Parameters
        ----------
        node : NodeT
            Parent node to transform *all* child nodes of.

        Returns
        -------
        NodeT
            Parent node returned and thus preserved as is.
        '''

        # Type of this parent node.
        node_type = type(node)

        # If this parent node declares a new lexical scope (i.e., by defining a
        # new class or callable)...
        if node_type in TYPES_NODE_LEXICAL_SCOPE:
            # Add the type of this parent node to the top of the stack of all
            # current lexical scopes *BEFORE* visiting any child nodes of this
            # parent node.
            self._scopes.append_scope_nested(
                # Fully-qualified name of the parent scope (i.e.,
                # "{self._scopes[-1].name}") followed by the unqualified
                # basename of this new class or callable declaring this new
                # lexical scope (i.e., "{node.name}").
                #
                # Note that both the "ast.ClassDef" *AND* "ast.FunctionDef" node
                # types define the "name" instance variable accessed here.
                name=f'{self._scopes[-1].name}.{node.name}',  # type: ignore[attr-defined]
                node_type=node_type,
            )

            # Recursively visit *ALL* child nodes of this parent node.
            super().generic_visit(node)

            # Remove the type of this parent node from the top of the stack of
            # all current lexical scopes *AFTER* visiting all child nodes of
            # this parent node.
            self._scopes.pop()
        # Else, this parent node does *NOT* declare a new lexical scope. In this
        # case...
        else:
            # Recursively visit all child nodes of this parent node *WITHOUT*.
            # modifying the stack of all current lexical scopes.
            super().generic_visit(node)

        # Return this parent node as is.
        return node

    # ..................{ VISITORS ~ class                   }..................
    #FIXME: Implement us up, please.
    def visit_ClassDef(self, node: ClassDef) -> Optional[ClassDef]:
        '''
        Add a new child node to the passed **class node** (i.e., node
        encapsulating the definition of a pure-Python class) unconditionally
        decorating that class by our private
        :func:`beartype._decor.decorcore.beartype_object_nonfatal` decorator.

        Parameters
        ----------
        node : ClassDef
            Class node to be transformed.

        Returns
        -------
        Optional[ClassDef]
            This same class node.
        '''

        # Add a new child decoration node to this parent class node decorating
        # this class by @beartype under this configuration.
        self._decorate_node_beartype(node=node, conf=self._conf)

        # Recursively transform *ALL* child nodes of this parent class node.
        # Note that doing so implicitly calls the visit_FunctionDef() method
        # (defined below), each of which then effectively reduces to a noop.
        return self.generic_visit(node)

    # ..................{ VISITORS ~ callable                }..................
    def visit_FunctionDef(self, node: NodeCallable) -> Optional[NodeCallable]:
        '''
        Add a new child node to the passed **callable node** (i.e., node
        encapsulating the definition of a pure-Python function or method)
        decorating that callable by our private
        :func:`beartype._decor.decorcore.beartype_object_nonfatal` decorator if
        that callable is **typed** (i.e., annotated by a return type hint and/or
        one or more parameter type hints).

        Parameters
        ----------
        node : NodeCallable
            Callable node to be transformed.

        Returns
        -------
        Optional[NodeCallable]
            This same callable node.
        '''

        # If...
        if (
            # * This callable node has one or more parent nodes previously
            #   visited by this node transformer *AND* the immediate parent node
            #   of this callable node is a class node, then this callable node
            #   encapsulates a method rather than a function. In this case, the
            #   visit_ClassDef() method defined above has already explicitly
            #   decorated the class defining this method by the @beartype
            #   decorator, which then implicitly decorates both this and all
            #   other methods of that class by that decorator. For safety and
            #   efficiency, avoid needlessly re-decorating this method by the
            #   same decorator by preserving and returning this node as is.
            # * That is *NOT* the case, then this callable node is either the
            #   root node of the current AST *OR* has a parent node that is not
            #   a class node. In either case, this callable node necessarily
            #   encapsulates a function (rather than a method), which yet to be
            #   decorated. Do so now! So say we all.
            #
            # This logic corresponds to the above "That is *NOT* the case" case
            # (i.e., this callable node necessarily encapsulates a function).
            # Look. Just accept that we have a tenuous grasp on reality at best.
            not self._scopes.is_scope_class and
            # ...and the currently visited callable is annotated by one or more
            # type hints and thus *NOT* ignorable with respect to beartype
            # decoration...
            is_node_callable_typed(node)
        ):
            # print(f'Decorating function {node.name}()...')

            # Add a new child decoration node to this parent callable node
            # decorating this callable by @beartype under this configuration.
            self._decorate_node_beartype(node=node, conf=self._conf)
        # Else, that callable is ignorable. In this case, avoid needlessly
        # decorating that callable by @beartype for efficiency.

        # Recursively transform *ALL* child nodes of this parent callable node.
        return self.generic_visit(node)


    # Efficiently decorate coroutines (i.e., asynchronous callables declared via
    # the "async" keyword) by aliasing the existing visit_FunctionDef() method
    # defined above (which directly handles *ONLY* synchronous callables) to a
    # new visit_AsyncFunctionDef (which directly handles *ONLY* asynchronous
    # callables). Since the visit_FunctionDef() implementation transparently
    # handles both synchronous and asynchronous callables, this is the optimally
    # efficient approach to decorate coroutines *WITHOUT* adding any additional
    # stack frames to the call stack.
    visit_AsyncFunctionDef = visit_FunctionDef
