1# Copyright (c) Meta Platforms, Inc. and affiliates.
2#
3# This source code is licensed under the MIT license found in the
4# LICENSE file in the root directory of this source tree.
5#
6from __future__ import annotations
7
8import argparse
9import inspect
10from abc import ABC, abstractmethod
11from typing import Dict, Generator, List, Tuple, Type, TypeVar
12
13from libcst import CSTNode, Module
14from libcst.codemod._codemod import Codemod
15from libcst.codemod._context import CodemodContext
16from libcst.codemod._visitor import ContextAwareTransformer
17from libcst.codemod.visitors._add_imports import AddImportsVisitor
18from libcst.codemod.visitors._remove_imports import RemoveImportsVisitor
19
20_Codemod = TypeVar("_Codemod", bound=Codemod)
21
22
23class CodemodCommand(Codemod, ABC):
24 """
25 A :class:`~libcst.codemod.Codemod` which can be invoked on the command-line
26 using the ``libcst.tool codemod`` utility. It behaves like any other codemod
27 in that it can be instantiated and run identically to a
28 :class:`~libcst.codemod.Codemod`. However, it provides support for providing
29 help text and command-line arguments to ``libcst.tool codemod`` as well as
30 facilities for automatically running certain common transforms after executing
31 your :meth:`~libcst.codemod.Codemod.transform_module_impl`.
32
33 The following list of transforms are automatically run at this time:
34
35 - :class:`~libcst.codemod.visitors.AddImportsVisitor` (adds needed imports to a module).
36 - :class:`~libcst.codemod.visitors.RemoveImportsVisitor` (removes unreferenced imports from a module).
37 """
38
39 #: An overrideable description attribute so that codemods can provide
40 #: a short summary of what they do. This description will show up in
41 #: command-line help as well as when listing available codemods.
42 DESCRIPTION: str = "No description."
43
44 @staticmethod
45 def add_args(arg_parser: argparse.ArgumentParser) -> None:
46 """
47 Override this to add arguments to the CLI argument parser. These args
48 will show up when the user invokes ``libcst.tool codemod`` with
49 ``--help``. They will also be presented to your class's ``__init__``
50 method. So, if you define a command with an argument 'foo', you should also
51 have a corresponding 'foo' positional or keyword argument in your
52 class's ``__init__`` method.
53 """
54
55 pass
56
57 def _instantiate_and_run(self, transform: Type[_Codemod], tree: Module) -> Module:
58 inst = transform(self.context)
59 return inst.transform_module(tree)
60
61 @abstractmethod
62 def transform_module_impl(self, tree: Module) -> Module:
63 """
64 Override this with your transform. You should take in the tree, optionally
65 mutate it and then return the mutated version. The module reference and all
66 calculated metadata are available for the lifetime of this function.
67 """
68 ...
69
70 # Lightweight wrappers for RemoveImportsVisitor static functions
71 def remove_unused_import(
72 self,
73 module: str,
74 obj: str | None = None,
75 asname: str | None = None,
76 ) -> None:
77 RemoveImportsVisitor.remove_unused_import(self.context, module, obj, asname)
78
79 def remove_unused_import_by_node(self, node: CSTNode) -> None:
80 RemoveImportsVisitor.remove_unused_import_by_node(self.context, node)
81
82 # Lightweight wrappers for AddImportsVisitor static functions
83 def add_needed_import(
84 self,
85 module: str,
86 obj: str | None = None,
87 asname: str | None = None,
88 relative: int = 0,
89 ) -> None:
90 AddImportsVisitor.add_needed_import(self.context, module, obj, asname, relative)
91
92 def transform_module(self, tree: Module) -> Module:
93 # Overrides (but then calls) Codemod's transform_module to provide
94 # a spot where additional supported transforms can be attached and run.
95 tree = super().transform_module(tree)
96
97 # List of transforms we should run, with their context key they use
98 # for storing in context.scratch. Typically, the transform will also
99 # have a static method that other transforms can use which takes
100 # a context and other optional args and modifies its own context key
101 # accordingly. We import them here so that we don't have circular imports.
102 supported_transforms: List[Tuple[str, Type[Codemod]]] = [
103 (AddImportsVisitor.CONTEXT_KEY, AddImportsVisitor),
104 (RemoveImportsVisitor.CONTEXT_KEY, RemoveImportsVisitor),
105 ]
106
107 # For any visitors that we support auto-running, run them here if needed.
108 for key, transform in supported_transforms:
109 if key in self.context.scratch:
110 # We have work to do, so lets run this.
111 tree = self._instantiate_and_run(transform, tree)
112
113 # We're finally done!
114 return tree
115
116
117class VisitorBasedCodemodCommand(ContextAwareTransformer, CodemodCommand, ABC):
118 """
119 A command that acts identically to a visitor-based transform, but also has
120 the support of :meth:`~libcst.codemod.CodemodCommand.add_args` and running
121 supported helper transforms after execution. See
122 :class:`~libcst.codemod.CodemodCommand` and
123 :class:`~libcst.codemod.ContextAwareTransformer` for additional documentation.
124 """
125
126 pass
127
128
129class MagicArgsCodemodCommand(CodemodCommand, ABC):
130 """
131 A "magic" args command, which auto-magically looks up the transforms that
132 are yielded from :meth:`~libcst.codemod.MagicArgsCodemodCommand.get_transforms`
133 and instantiates them using values out of the context. Visitors yielded in
134 :meth:`~libcst.codemod.MagicArgsCodemodCommand.get_transforms` must have
135 constructor arguments that match a key in the context
136 :attr:`~libcst.codemod.CodemodContext.scratch`. The easiest way to
137 guarantee that is to use :meth:`~libcst.codemod.CodemodCommand.add_args`
138 to add a command arg that will be parsed for each of the args. However, if
139 you wish to chain transforms, adding to the scratch in one transform will make
140 the value available to the constructor in subsequent transforms as well as the
141 scratch for subsequent transforms.
142 """
143
144 def __init__(self, context: CodemodContext, **kwargs: Dict[str, object]) -> None:
145 super().__init__(context)
146 self.context.scratch.update(kwargs)
147
148 @abstractmethod
149 def get_transforms(self) -> Generator[Type[Codemod], None, None]:
150 """
151 A generator which yields one or more subclasses of
152 :class:`~libcst.codemod.Codemod`. In the general case, you will usually
153 yield a series of classes, but it is possible to programmatically decide
154 which classes to yield depending on the contents of the context
155 :attr:`~libcst.codemod.CodemodContext.scratch`.
156
157 Note that you should yield classes, not instances of classes, as the
158 point of :class:`~libcst.codemod.MagicArgsCodemodCommand` is to
159 instantiate them for you with the contents of
160 :attr:`~libcst.codemod.CodemodContext.scratch`.
161 """
162 ...
163
164 def _instantiate(self, transform: Type[_Codemod]) -> _Codemod:
165 # Grab the expected arguments
166 argspec = inspect.getfullargspec(transform.__init__)
167 args: List[object] = []
168 kwargs: Dict[str, object] = {}
169 last_default_arg = len(argspec.args) - len(argspec.defaults or ())
170 for i, arg in enumerate(argspec.args):
171 if arg in ["self", "context"]:
172 # Self is bound, and context we explicitly include below.
173 continue
174 if arg not in self.context.scratch:
175 if i >= last_default_arg:
176 # This arg has a default, so the fact that its missing is fine.
177 continue
178 raise KeyError(
179 f"Visitor {transform.__name__} requires positional arg {arg} but "
180 + "it is not in our context nor does it have a default! It should "
181 + "be provided by an argument returned from the 'add_args' method "
182 + "or populated into context.scratch by a previous transform!"
183 )
184 # No default, but we found something in scratch. So, forward it.
185 args.append(self.context.scratch[arg])
186 kwonlydefaults = argspec.kwonlydefaults or {}
187 for kwarg in argspec.kwonlyargs:
188 if kwarg not in self.context.scratch and kwarg not in kwonlydefaults:
189 raise KeyError(
190 f"Visitor {transform.__name__} requires keyword arg {kwarg} but "
191 + "it is not in our context nor does it have a default! It should "
192 + "be provided by an argument returned from the 'add_args' method "
193 + "or populated into context.scratch by a previous transform!"
194 )
195 kwargs[kwarg] = self.context.scratch.get(kwarg, kwonlydefaults[kwarg])
196
197 # Return an instance of the transform with those arguments
198 return transform(self.context, *args, **kwargs)
199
200 def transform_module_impl(self, tree: Module) -> Module:
201 for transform in self.get_transforms():
202 inst = self._instantiate(transform)
203 tree = inst.transform_module(tree)
204 return tree