# Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. # """ Provides everything needed to run a CodemodCommand. """ import traceback from dataclasses import dataclass from enum import Enum from typing import Optional, Sequence, Union from libcst import parse_module, PartialParserConfig from libcst.codemod._codemod import Codemod # All datastructures defined in this class are pickleable so that they can be used # as a return value with the multiprocessing module. @dataclass(frozen=True) class TransformSuccess: """ A :class:`~libcst.codemod.TransformResult` used when the codemod was successful. Stores all the information we might need to display to the user upon success, as well as the transformed file contents. """ #: All warning messages that were generated during the codemod. warning_messages: Sequence[str] #: The updated code, post-codemod. code: str @dataclass(frozen=True) class TransformFailure: """ A :class:`~libcst.codemod.TransformResult` used when the codemod failed. Stores all the information we might need to display to the user upon a failure. """ #: All warning messages that were generated before the codemod crashed. warning_messages: Sequence[str] #: The exception that was raised during the codemod. error: Exception #: The traceback string that was recorded at the time of exception. traceback_str: str @dataclass(frozen=True) class TransformExit: """ A :class:`~libcst.codemod.TransformResult` used when the codemod was interrupted by the user (e.g. KeyboardInterrupt). """ #: An empty list of warnings, included so that all #: :class:`~libcst.codemod.TransformResult` have a ``warning_messages`` attribute. warning_messages: Sequence[str] = () class SkipReason(Enum): """ An enumeration of all valid reasons for a codemod to skip. """ #: The module was skipped because we detected that it was generated code, and #: we were configured to skip generated files. GENERATED = "generated" #: The module was skipped because we detected that it was blacklisted, and we #: were configured to skip blacklisted files. BLACKLISTED = "blacklisted" #: The module was skipped because the codemod requested us to skip using the #: :class:`~libcst.codemod.SkipFile` exception. OTHER = "other" @dataclass(frozen=True) class TransformSkip: """ A :class:`~libcst.codemod.TransformResult` used when the codemod requested to be skipped. This could be because it's a generated file, or due to filename blacklist, or because the transform raised :class:`~libcst.codemod.SkipFile`. """ #: The reason that we skipped codemodding this module. skip_reason: SkipReason #: The description populated from the :class:`~libcst.codemod.SkipFile` exception. skip_description: str #: All warning messages that were generated before the codemod decided to skip. warning_messages: Sequence[str] = () class SkipFile(Exception): """ Raise this exception to skip codemodding the current file. The exception message should be the reason for skipping. """ TransformResult = Union[ TransformSuccess, TransformFailure, TransformExit, TransformSkip ] def transform_module( transformer: Codemod, code: str, *, python_version: Optional[str] = None ) -> TransformResult: """ Given a module as represented by a string and a :class:`~libcst.codemod.Codemod` that we wish to run, execute the codemod on the code and return a :class:`~libcst.codemod.TransformResult`. This should never raise an exception. On success, this returns a :class:`~libcst.codemod.TransformSuccess` containing any generated warnings as well as the transformed code. If the codemod is interrupted with a Ctrl+C, this returns a :class:`~libcst.codemod.TransformExit`. If the codemod elected to skip by throwing a :class:`~libcst.codemod.SkipFile` exception, this will return a :class:`~libcst.codemod.TransformSkip` containing the reason for skipping as well as any warnings that were generated before the codemod decided to skip. If the codemod throws an unexpected exception, this will return a :class:`~libcst.codemod.TransformFailure` containing the exception that occured as well as any warnings that were generated before the codemod crashed. """ try: input_tree = parse_module( code, config=( PartialParserConfig(python_version=python_version) if python_version is not None else PartialParserConfig() ), ) output_tree = transformer.transform_module(input_tree) return TransformSuccess( code=output_tree.code, warning_messages=transformer.context.warnings ) except KeyboardInterrupt: return TransformExit() except SkipFile as ex: return TransformSkip( skip_description=str(ex), skip_reason=SkipReason.OTHER, warning_messages=transformer.context.warnings, ) except Exception as ex: return TransformFailure( error=ex, traceback_str=traceback.format_exc(), warning_messages=transformer.context.warnings, )