CodeShell.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <?php
  2. App::uses('AppShell', 'Console/Command');
  3. if (!defined('LF')) {
  4. define('LF', PHP_EOL); # use PHP to detect default linebreak
  5. }
  6. /**
  7. * Misc Code Fix Tools
  8. *
  9. * cake Tools.Code dependencies [-p PluginName] [-c /custom/path]
  10. * - Fix missing App::uses() statements
  11. *
  12. * @author Mark Scherer
  13. * @license http://opensource.org/licenses/mit-license.php MIT
  14. */
  15. class CodeShell extends AppShell {
  16. protected $_files;
  17. protected $_paths;
  18. /**
  19. * Detect and fix class dependencies
  20. *
  21. * @return void
  22. */
  23. public function dependencies() {
  24. if ($customPath = $this->params['custom']) {
  25. $this->_paths = [$customPath];
  26. } elseif (!empty($this->params['plugin'])) {
  27. $this->_paths = [CakePlugin::path($this->params['plugin'])];
  28. } else {
  29. $this->_paths = [APP];
  30. }
  31. $this->_findFiles('php');
  32. foreach ($this->_files as $file) {
  33. $this->out(sprintf('Updating %s...', $file), 1, Shell::VERBOSE);
  34. $this->_correctFile($file);
  35. $this->out(sprintf('Done updating %s', $file), 1, Shell::VERBOSE);
  36. }
  37. }
  38. /**
  39. * @return void
  40. */
  41. protected function _correctFile($file) {
  42. $fileContent = $content = file_get_contents($file);
  43. preg_match_all('/class \w+ extends (.+)\s*{/', $fileContent, $matches);
  44. if (empty($matches)) {
  45. return;
  46. }
  47. $excludes = ['Fixture', 'Exception', 'TestSuite', 'CakeTestModel'];
  48. $missingClasses = [];
  49. foreach ($matches[1] as $match) {
  50. $match = trim($match);
  51. preg_match('/\bApp\:\:uses\(\'' . $match . '\'/', $fileContent, $usesMatches);
  52. if (!empty($usesMatches)) {
  53. continue;
  54. }
  55. preg_match('/class ' . $match . '\s*(w+)?{/', $fileContent, $existingMatches);
  56. if (!empty($existingMatches)) {
  57. continue;
  58. }
  59. if (in_array($match, $missingClasses)) {
  60. continue;
  61. }
  62. $break = false;
  63. foreach ($excludes as $exclude) {
  64. if (strposReverse($match, $exclude) === 0) {
  65. $break = true;
  66. break;
  67. }
  68. }
  69. if ($break) {
  70. continue;
  71. }
  72. $missingClasses[] = $match;
  73. }
  74. if (empty($missingClasses)) {
  75. return;
  76. }
  77. $fileContent = explode(LF, $fileContent);
  78. $inserted = [];
  79. $pos = 1;
  80. if (!empty($fileContent[1]) && $fileContent[1] === '/**') {
  81. $count = count($fileContent);
  82. for ($i = $pos; $i < $count - 1; $i++) {
  83. if (strpos($fileContent[$i], '*/') !== false) {
  84. if (strpos($fileContent[$i + 1], 'class ') !== 0) {
  85. $pos = $i + 1;
  86. }
  87. break;
  88. }
  89. }
  90. }
  91. // try to find the best position to insert app uses statements
  92. foreach ($fileContent as $row => $rowValue) {
  93. preg_match('/^App\:\:uses\(/', $rowValue, $matches);
  94. if ($matches) {
  95. $pos = $row;
  96. break;
  97. }
  98. }
  99. foreach ($missingClasses as $missingClass) {
  100. $classes = [
  101. 'Controller' => 'Controller',
  102. 'Component' => 'Controller/Component',
  103. 'Shell' => 'Console/Command',
  104. 'Model' => 'Model',
  105. 'Behavior' => 'Model/Behavior',
  106. 'Datasource' => 'Model/Datasource',
  107. 'Task' => 'Console/Command/Task',
  108. 'View' => 'View',
  109. 'Helper' => 'View/Helper',
  110. ];
  111. $type = null;
  112. foreach ($classes as $class => $namespace) {
  113. if (($t = strposReverse($missingClass, $class)) === 0) {
  114. $type = $namespace;
  115. break;
  116. }
  117. }
  118. if (empty($type)) {
  119. $this->err($missingClass . ' (' . $file . ') could not be matched');
  120. continue;
  121. }
  122. if ($class === 'Model') {
  123. $missingClassName = $missingClass;
  124. } else {
  125. $missingClassName = substr($missingClass, 0, strlen($missingClass) - strlen($class));
  126. }
  127. $objects = App::objects(($this->params['plugin'] ? $this->params['plugin'] . '.' : '') . $class);
  128. //FIXME: correct result for plugin classes
  129. if ($missingClass === 'Component') {
  130. $type = 'Controller';
  131. } elseif ($missingClass === 'Helper') {
  132. $type = 'View';
  133. } elseif ($missingClass === 'ModelBehavior') {
  134. $type = 'Model';
  135. } elseif (!empty($this->params['plugin']) && ($location = App::location($missingClass))) {
  136. $type = $location;
  137. } elseif (in_array($missingClass, $objects)) {
  138. $type = ($this->params['plugin'] ? ($this->params['plugin'] . '.') : '') . $type;
  139. }
  140. $inserted[] = 'App::uses(\'' . $missingClass . '\', \'' . $type . '\');';
  141. }
  142. if (!$inserted) {
  143. return;
  144. }
  145. array_splice($fileContent, $pos, 0, $inserted);
  146. $fileContent = implode(LF, $fileContent);
  147. if (empty($this->params['dry-run'])) {
  148. file_put_contents($file, $fileContent);
  149. $this->out(sprintf('Correcting %s', $file), 1, Shell::VERBOSE);
  150. }
  151. }
  152. /**
  153. * Make sure all files are properly encoded (ü instead of &uuml; etc)
  154. * FIXME: non-utf8 files to utf8 files error on windows!
  155. *
  156. * @return void
  157. */
  158. public function utf8() {
  159. $this->_paths = [APP . 'View' . DS];
  160. $this->params['ext'] = 'php|ctp';
  161. //$this->out('found: '.count($this->_files));
  162. $patterns = [
  163. ];
  164. $umlauts = ['ä', 'ö', 'ü', 'Ä', 'Ö', 'Ü', 'ß'];
  165. foreach ($umlauts as $umlaut) {
  166. $patterns[] = [
  167. ent($umlaut) . ' => ' . $umlaut,
  168. '/' . ent($umlaut) . '/',
  169. $umlaut,
  170. ];
  171. }
  172. $this->_filesRegexpUpdate($patterns);
  173. }
  174. /**
  175. * CodeShell::_filesRegexpUpdate()
  176. *
  177. * @param mixed $patterns
  178. * @return void
  179. */
  180. protected function _filesRegexpUpdate($patterns) {
  181. $this->_findFiles($this->params['ext']);
  182. foreach ($this->_files as $file) {
  183. $this->out(sprintf('Updating %s...', $file), 1, Shell::VERBOSE);
  184. $this->_utf8File($file, $patterns);
  185. }
  186. }
  187. /**
  188. * Searches the paths and finds files based on extension.
  189. *
  190. * @param string $extensions
  191. * @return void
  192. */
  193. protected function _findFiles($extensions = '') {
  194. $this->_files = [];
  195. foreach ($this->_paths as $path) {
  196. if (!is_dir($path)) {
  197. continue;
  198. }
  199. $Iterator = new RegexIterator(
  200. new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)),
  201. '/^.+\.(' . $extensions . ')$/i',
  202. RegexIterator::MATCH
  203. );
  204. foreach ($Iterator as $file) {
  205. $excludes = ['Config'];
  206. //Iterator processes plugins even if not asked to
  207. if (empty($this->params['plugin'])) {
  208. $excludes = array_merge($excludes, ['Plugin', 'plugins']);
  209. }
  210. if (empty($this->params['vendor'])) {
  211. $excludes = array_merge($excludes, ['Vendor', 'vendors']);
  212. }
  213. if (!empty($excludes)) {
  214. $isIllegalPluginPath = false;
  215. foreach ($excludes as $exclude) {
  216. if (strpos($file, $path . $exclude . DS) === 0) {
  217. $isIllegalPluginPath = true;
  218. break;
  219. }
  220. }
  221. if ($isIllegalPluginPath) {
  222. continue;
  223. }
  224. }
  225. if ($file->isFile()) {
  226. $this->_files[] = $file->getPathname();
  227. }
  228. }
  229. }
  230. }
  231. /**
  232. * CodeShell::_utf8File()
  233. *
  234. * @param mixed $file
  235. * @param mixed $patterns
  236. * @return void
  237. */
  238. protected function _utf8File($file, $patterns) {
  239. $contents = $fileContent = file_get_contents($file);
  240. foreach ($patterns as $pattern) {
  241. $this->out(sprintf(' * Updating %s', $pattern[0]), 1, Shell::VERBOSE);
  242. $contents = preg_replace($pattern[1], $pattern[2], $contents);
  243. }
  244. $this->out(sprintf('Done updating %s', $file), 1, Shell::VERBOSE);
  245. if (!$this->params['dry-run'] && $contents !== $fileContent) {
  246. if (file_exists($file)) {
  247. unlink($file);
  248. }
  249. if (WINDOWS) {
  250. //$fileContent = utf8_decode($fileContent);
  251. }
  252. file_put_contents($file, $contents);
  253. }
  254. }
  255. public function getOptionParser() {
  256. $subcommandParser = [
  257. 'options' => [
  258. 'plugin' => [
  259. 'short' => 'p',
  260. 'help' => 'The plugin to update. Only the specified plugin will be updated.',
  261. 'default' => ''
  262. ],
  263. 'custom' => [
  264. 'short' => 'c',
  265. 'help' => 'Custom path to update recursivly.',
  266. 'default' => ''
  267. ],
  268. 'ext' => [
  269. 'short' => 'e',
  270. 'help' => 'The extension(s) to search. A pipe delimited list, or a preg_match compatible subpattern',
  271. 'default' => 'php|ctp|thtml|inc|tpl'
  272. ],
  273. 'vendor' => [
  274. 'short' => 'e',
  275. 'help' => 'Include vendor files, as well',
  276. 'boolean' => true
  277. ],
  278. 'dry-run' => [
  279. 'short' => 'd',
  280. 'help' => 'Dry run the update, no files will actually be modified.',
  281. 'boolean' => true
  282. ]
  283. ]
  284. ];
  285. return parent::getOptionParser()
  286. ->description("A shell to help automate code cleanup. \n" .
  287. "Be sure to have a backup of your application before running these commands.")
  288. ->addSubcommand('group', [
  289. 'help' => 'Run multiple commands.',
  290. 'parser' => $subcommandParser
  291. ])
  292. ->addSubcommand('dependencies', [
  293. 'help' => 'Correct dependencies',
  294. 'parser' => $subcommandParser
  295. ])
  296. ->addSubcommand('utf8', [
  297. 'help' => 'Make files utf8 compliant',
  298. 'parser' => $subcommandParser
  299. ]);
  300. }
  301. /**
  302. * Shell tasks
  303. *
  304. * @var array
  305. */
  306. public $tasks = [
  307. 'CodeConvention',
  308. 'CodeWhitespace'
  309. ];
  310. /**
  311. * Main execution function
  312. *
  313. * @return void
  314. */
  315. public function group() {
  316. if (!empty($this->args)) {
  317. if (!empty($this->args[1])) {
  318. $this->args[1] = constant($this->args[1]);
  319. } else {
  320. $this->args[1] = APP;
  321. }
  322. $this->{'Code' . ucfirst($this->args[0])}->execute($this->args[1]);
  323. } else {
  324. $this->out('Usage: cake code type');
  325. $this->out('');
  326. $this->out('type should be space-separated');
  327. $this->out('list of any combination of:');
  328. $this->out('');
  329. $this->out('convention');
  330. $this->out('whitespace');
  331. }
  332. }
  333. }
  334. function strposReverse($str, $search) {
  335. $str = strrev($str);
  336. $search = strrev($search);
  337. $posRev = strpos($str, $search);
  338. return $posRev;
  339. }