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 """
78 Schedule an import to be removed after the codemod completes.
79
80 This is a convenience wrapper around the :class:`~libcst.codemod.visitors.RemoveImportsVisitor` static function
81 :meth:`~libcst.codemod.visitors.RemoveImportsVisitor.remove_unused_import`
82 that automatically passes the codemod context. The import will only be
83 removed if it is not referenced elsewhere in the module.
84
85 For example, to remove ``from typing import Optional``::
86
87 self.remove_unused_import("typing", "Optional")
88
89 To remove ``import os``::
90
91 self.remove_unused_import("os")
92 """
93 RemoveImportsVisitor.remove_unused_import(self.context, module, obj, asname)
94
95 def remove_unused_import_by_node(self, node: CSTNode) -> None:
96 """
97 Schedule removal of all imports referenced by a node and its children.
98
99 This is a convenience wrapper around the :class:`~libcst.codemod.visitors.RemoveImportsVisitor` static function
100 :meth:`~libcst.codemod.visitors.RemoveImportsVisitor.remove_unused_import_by_node`
101 that automatically passes the codemod context. This is especially useful
102 when you are removing a node using :func:`~libcst.RemoveFromParent` and want
103 to clean up any imports that were only used by that node.
104
105 For example::
106
107 def leave_AnnAssign(
108 self, original_node: cst.AnnAssign, updated_node: cst.AnnAssign,
109 ) -> cst.RemovalSentinel:
110 # Remove annotated assignment and clean up imports
111 self.remove_unused_import_by_node(original_node)
112 return cst.RemoveFromParent()
113
114 Note that you should pass the ``original_node`` rather than ``updated_node``
115 since scope analysis is computed on the original tree.
116 """
117 RemoveImportsVisitor.remove_unused_import_by_node(self.context, node)
118
119 # Lightweight wrappers for AddImportsVisitor static functions
120 def add_needed_import(
121 self,
122 module: str,
123 obj: str | None = None,
124 asname: str | None = None,
125 relative: int = 0,
126 ) -> None:
127 """
128 Schedule an import to be added after the codemod completes.
129
130 This is a convenience wrapper around the :class:`~libcst.codemod.visitors.AddImportsVisitor` static function
131 :meth:`~libcst.codemod.visitors.AddImportsVisitor.add_needed_import`
132 that automatically passes the codemod context. The import will only be
133 added if it does not already exist in the module.
134
135 For example, to add ``from typing import Optional``::
136
137 self.add_needed_import("typing", "Optional")
138
139 To add ``import os``::
140
141 self.add_needed_import("os")
142
143 To add ``from typing import List as L``::
144
145 self.add_needed_import("typing", "List", asname="L")
146 """
147 AddImportsVisitor.add_needed_import(self.context, module, obj, asname, relative)
148
149 def transform_module(self, tree: Module) -> Module:
150 # Overrides (but then calls) Codemod's transform_module to provide
151 # a spot where additional supported transforms can be attached and run.
152 tree = super().transform_module(tree)
153
154 # List of transforms we should run, with their context key they use
155 # for storing in context.scratch. Typically, the transform will also
156 # have a static method that other transforms can use which takes
157 # a context and other optional args and modifies its own context key
158 # accordingly. We import them here so that we don't have circular imports.
159 supported_transforms: List[Tuple[str, Type[Codemod]]] = [
160 (AddImportsVisitor.CONTEXT_KEY, AddImportsVisitor),
161 (RemoveImportsVisitor.CONTEXT_KEY, RemoveImportsVisitor),
162 ]
163
164 # For any visitors that we support auto-running, run them here if needed.
165 for key, transform in supported_transforms:
166 if key in self.context.scratch:
167 # We have work to do, so lets run this.
168 tree = self._instantiate_and_run(transform, tree)
169
170 # We're finally done!
171 return tree
172
173
174class VisitorBasedCodemodCommand(ContextAwareTransformer, CodemodCommand, ABC):
175 """
176 A command that acts identically to a visitor-based transform, but also has
177 the support of :meth:`~libcst.codemod.CodemodCommand.add_args` and running
178 supported helper transforms after execution. See
179 :class:`~libcst.codemod.CodemodCommand` and
180 :class:`~libcst.codemod.ContextAwareTransformer` for additional documentation.
181 """
182
183 pass
184
185
186class MagicArgsCodemodCommand(CodemodCommand, ABC):
187 """
188 A "magic" args command, which auto-magically looks up the transforms that
189 are yielded from :meth:`~libcst.codemod.MagicArgsCodemodCommand.get_transforms`
190 and instantiates them using values out of the context. Visitors yielded in
191 :meth:`~libcst.codemod.MagicArgsCodemodCommand.get_transforms` must have
192 constructor arguments that match a key in the context
193 :attr:`~libcst.codemod.CodemodContext.scratch`. The easiest way to
194 guarantee that is to use :meth:`~libcst.codemod.CodemodCommand.add_args`
195 to add a command arg that will be parsed for each of the args. However, if
196 you wish to chain transforms, adding to the scratch in one transform will make
197 the value available to the constructor in subsequent transforms as well as the
198 scratch for subsequent transforms.
199 """
200
201 def __init__(self, context: CodemodContext, **kwargs: Dict[str, object]) -> None:
202 super().__init__(context)
203 self.context.scratch.update(kwargs)
204
205 @abstractmethod
206 def get_transforms(self) -> Generator[Type[Codemod], None, None]:
207 """
208 A generator which yields one or more subclasses of
209 :class:`~libcst.codemod.Codemod`. In the general case, you will usually
210 yield a series of classes, but it is possible to programmatically decide
211 which classes to yield depending on the contents of the context
212 :attr:`~libcst.codemod.CodemodContext.scratch`.
213
214 Note that you should yield classes, not instances of classes, as the
215 point of :class:`~libcst.codemod.MagicArgsCodemodCommand` is to
216 instantiate them for you with the contents of
217 :attr:`~libcst.codemod.CodemodContext.scratch`.
218 """
219 ...
220
221 def _instantiate(self, transform: Type[_Codemod]) -> _Codemod:
222 # Grab the expected arguments
223 argspec = inspect.getfullargspec(transform.__init__)
224 args: List[object] = []
225 kwargs: Dict[str, object] = {}
226 last_default_arg = len(argspec.args) - len(argspec.defaults or ())
227 for i, arg in enumerate(argspec.args):
228 if arg in ["self", "context"]:
229 # Self is bound, and context we explicitly include below.
230 continue
231 if arg not in self.context.scratch:
232 if i >= last_default_arg:
233 # This arg has a default, so the fact that its missing is fine.
234 continue
235 raise KeyError(
236 f"Visitor {transform.__name__} requires positional arg {arg} but "
237 + "it is not in our context nor does it have a default! It should "
238 + "be provided by an argument returned from the 'add_args' method "
239 + "or populated into context.scratch by a previous transform!"
240 )
241 # No default, but we found something in scratch. So, forward it.
242 args.append(self.context.scratch[arg])
243 kwonlydefaults = argspec.kwonlydefaults or {}
244 for kwarg in argspec.kwonlyargs:
245 if kwarg not in self.context.scratch and kwarg not in kwonlydefaults:
246 raise KeyError(
247 f"Visitor {transform.__name__} requires keyword arg {kwarg} but "
248 + "it is not in our context nor does it have a default! It should "
249 + "be provided by an argument returned from the 'add_args' method "
250 + "or populated into context.scratch by a previous transform!"
251 )
252 kwargs[kwarg] = self.context.scratch.get(kwarg, kwonlydefaults[kwarg])
253
254 # Return an instance of the transform with those arguments
255 return transform(self.context, *args, **kwargs)
256
257 def transform_module_impl(self, tree: Module) -> Module:
258 for transform in self.get_transforms():
259 inst = self._instantiate(transform)
260 tree = inst.transform_module(tree)
261 return tree