-
Notifications
You must be signed in to change notification settings - Fork 1
/
CodemodRunner.php
229 lines (197 loc) · 8.05 KB
/
CodemodRunner.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<?php
namespace Codeshift;
use \Codeshift\Exceptions\{FileNotFoundException, CorruptCodemodException, CodeParsingException};
/**
* Main class for loading and executing an arbitrary number of codemods
* on a source file or directory.
*/
class CodemodRunner {
private $oTracer;
private $oTransformer;
private $codemodPaths = [];
private $codemodClasses = [];
private $codemodOptions = [];
/**
* Constructor.
* Optionally takes an initial codemod to be added to execution schedule
* and a custom tracer to be used for protocolling.
* By default, the `SimpleOutputTracer` is used.
*
* @param string|null $codemodFilePath Optional path of initial codemod file
* @param AbstractTracer|null $oTracer Optional custom tracer to use
*/
public function __construct($codemodFilePath=null, AbstractTracer $oTracer=null) {
$this->oTracer = $oTracer ?: new SimpleOutputTracer();
$this->oTransformer = new CodeTransformer($this->oTracer);
if ($codemodFilePath) {
$this->addCodemod($codemodFilePath);
}
}
/**
* Sets an arbitrary list of custom options to be passed to each codemod on initialize.
* The options can be read within a codemod by using the method {@see AbstractCodemod::getOptions()}.
*
* @param array $options List of custom options
* @return void
*/
public function setCodemodOptions(array $options) {
$this->codemodOptions = $options;
}
/**
* Adds a codemod defintion file to be executed.
*
* @param string $filePath Path of codemod file
* @return void
*/
public function addCodemod($filePath) {
$this->codemodPaths[] = $filePath;
}
/**
* Adds multiple codemod defintion files to be executed.
*
* @param string[] $filePaths List of paths of codemod files
* @return void
*/
public function addCodemods(array $filePaths) {
foreach ($filePaths as $filePath) {
$this->addCodemod($filePath);
}
}
/**
* Removes all added codemod definition files (from execution schedule).
*
* @return void
*/
public function clearCodemods() {
$this->codemodPaths = [];
}
/**
* Loads the given codemod and prepares the code transformation routines from it.
* Note that a codemod is cached by its given path to avoid redeclarations of its class.
*
* @param string $codemodFilePath Path of codemod file
* @throws FileNotFoundException If the codemod cannot be not found
* @throws CorruptCodemodException If the codemod cannot be used for the preparation
* @return CodeTransformer The final prepared code transformer
*/
public function loadCodemodToTransformer($codemodFilePath) {
$codemodClass = null;
$loadedFromCache = false;
// Load codemod class
if (!isset($this->codemodClasses[$codemodFilePath])) {
if (file_exists($codemodFilePath)) {
$codemodClass = require $codemodFilePath;
if (!class_exists($codemodClass)) {
throw new CorruptCodemodException("Missing exported class of codemod: \"{$codemodFilePath}\"");
}
}
else {
throw new FileNotFoundException("Could not find codemod \"{$codemodFilePath}\"");
}
$this->codemodClasses[$codemodFilePath] = $codemodClass;
}
else {
$codemodClass = $this->codemodClasses[$codemodFilePath];
$loadedFromCache = true;
}
// Init the codemod
try {
$oCodemod = new $codemodClass($this->oTracer, $this->codemodOptions);
}
catch (\Exception $ex) {
throw new CorruptCodemodException("Failed to init codemod \"{$codemodFilePath}\" :: {$ex->getMessage()}", null, $ex);
}
// Prepare the transformer
if (is_subclass_of($oCodemod, '\Codeshift\AbstractCodemod')) {
$this->oTransformer->setCodemod($oCodemod);
}
else {
throw new CorruptCodemodException("Invalid class type of codemod: \"{$codemodFilePath}\"");
}
// Log loading
$this->oTracer->traceCodemodLoaded($oCodemod, realpath($codemodFilePath), $loadedFromCache);
return $this->oTransformer;
}
/**
* Transforms the source code of the given target path
* by executing all scheduled codemods (in same order they were added).
*
* Each codemod is newly instantiated before executing it.
*
* If no output path is given, the input source is updated directly by the result source.
* Else, if an output path is given, the input source remains untouched.
* All codemods following the first one are executed on the output path.
*
* Files matching the ignore paths are not written to output path.
*
* @param string $targetPath Path of the input source file or directory
* @param string $outputPath Path of the output file or directory
* @param string[] $ignorePaths List of file or directory paths to exclude from transformation
* @throws FileNotFoundException If a codemod cannot be found
* @throws CorruptCodemodException If a codemod cannot be interpreted
* @throws CodeParsingException If the source file cannot be parsed
* @return void
*/
public function execute($targetPath, $outputPath=null, array $ignorePaths=[]) {
$absoluteIgnorePaths = self::resolveRelativePaths($ignorePaths, $targetPath);
$currTargetPath = $targetPath;
$currOutputPath = $outputPath;
foreach ($this->codemodPaths as $codemodFilePath) {
$oTransformer = $this->loadCodemodToTransformer($codemodFilePath);
$resultOutputPath = $oTransformer->runOnPath($currTargetPath, $currOutputPath, $absoluteIgnorePaths);
// Execute further codemods on output path
if($outputPath != null) {
$currTargetPath = $resultOutputPath;
$currOutputPath = null;
}
}
}
/**
* Calls `execute` within a try-catch block to allow logging exceptions by the tracer.
*
* @see CodemodRunner::execute()
* @param bool $traceStack Set true to log the stack trace of an exception
* @return bool Whether or not the execution was completed succesfully
*/
public function executeSecured($targetPath, $outputPath=null, array $ignorePaths=[], $traceStack=false) {
try {
$this->execute($targetPath, $outputPath, $ignorePaths);
return true;
}
catch (\Exception $ex) {
$this->oTracer->traceException($ex, $traceStack);
}
return false;
}
/**
* Tries to resolve the given list of paths by using the given target as root.
* The target root must exist.
* Paths are only changed, if they specify existing files or directories.
*
* @param string[] $paths List of paths to resolve
* @param string $targetRootPath Path to use as root directory for relative paths
* @return string[] The resulting list of paths
*/
public static function resolveRelativePaths(array $paths, $targetRootPath) {
// If root path is file, use its directory
if (!is_dir($targetRootPath) AND file_exists($targetRootPath)) {
$targetRootPath = dirname($targetRootPath);
}
$targetRootPath = realpath($targetRootPath);
// Try to resolve relative paths
if ($targetRootPath !== false) {
foreach ($paths as &$wildPath) {
$absolutePath = $wildPath;
if (in_array(substr($wildPath, 0, 2), ['./', '.\\', '.'.DIRECTORY_SEPARATOR])) {
$absolutePath = $targetRootPath.DIRECTORY_SEPARATOR.$wildPath;
}
$realPath = realpath($absolutePath);
// Only change input path, if it was resolved successfully
if ($realPath !== false) {
$wildPath = $realPath;
}
}
}
return $paths;
}
}