# 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.
#
from abc import ABC, abstractmethod
from contextlib import contextmanager
from dataclasses import replace
from typing import Generator
from libcst import MetadataDependent, MetadataWrapper, Module
from libcst.codemod._context import CodemodContext
[docs]class Codemod(MetadataDependent, ABC):
"""
Abstract base class that all codemods must subclass from. Classes wishing
to perform arbitrary, non-visitor-based mutations on a tree should subclass
from this class directly. Classes wishing to perform visitor-based mutation
should instead subclass from :class:`~libcst.codemod.ContextAwareTransformer`.
Note that a :class:`~libcst.codemod.Codemod` is a subclass of
:class:`~libcst.MetadataDependent`, meaning that you can declare metadata
dependencies with the :attr:`~libcst.MetadataDependent.METADATA_DEPENDENCIES`
class property and while you are executing a transform you can call
:meth:`~libcst.MetadataDependent.get_metadata` to retrieve
the resolved metadata.
"""
def __init__(self, context: CodemodContext) -> None:
MetadataDependent.__init__(self)
self.context: CodemodContext = context
[docs] def should_allow_multiple_passes(self) -> bool:
"""
Override this and return ``True`` to allow your transform to be called
repeatedly until the tree doesn't change between passes. By default,
this is off, and should suffice for most transforms.
"""
return False
[docs] def warn(self, warning: str) -> None:
"""
Emit a warning that is displayed to the user who has invoked this codemod.
"""
self.context.warnings.append(warning)
@property
def module(self) -> Module:
"""
Reference to the currently-traversed module. Note that this is only available
during the execution of a codemod. The module reference is particularly
handy if you want to use :meth:`libcst.Module.code_for_node` or
:attr:`libcst.Module.config_for_parsing` and don't wish to track a reference
to the top-level module manually.
"""
module = self.context.module
if module is None:
raise Exception(
f"Attempted access of {self.__class__.__name__}.module outside of "
+ "transform_module()."
)
return module
@contextmanager
def _handle_metadata_reference(
self, module: Module
) -> Generator[Module, None, None]:
oldwrapper = self.context.wrapper
metadata_manager = self.context.metadata_manager
filename = self.context.filename
if metadata_manager is not None and filename:
# We can look up full-repo metadata for this codemod!
cache = metadata_manager.get_cache_for_path(filename)
wrapper = MetadataWrapper(module, cache=cache)
else:
# We are missing either the repo manager or the current path,
# which can happen when we are codemodding from stdin or when
# an upstream dependency manually instantiates us.
wrapper = MetadataWrapper(module)
with self.resolve(wrapper):
self.context = replace(self.context, wrapper=wrapper)
try:
yield wrapper.module
finally:
self.context = replace(self.context, wrapper=oldwrapper)