PageRenderTime 26ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 0ms

/laravel_tintuc/vendor/psy/psysh/src/CodeCleaner.php

https://gitlab.com/nmhieucoder/laravel_tintuc
PHP | 403 lines | 234 code | 48 blank | 121 comment | 17 complexity | 5d897bc175586f1f403b0630f873fa38 MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of Psy Shell.
  4. *
  5. * (c) 2012-2020 Justin Hileman
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Psy;
  11. use PhpParser\NodeTraverser;
  12. use PhpParser\Parser;
  13. use PhpParser\PrettyPrinter\Standard as Printer;
  14. use Psy\CodeCleaner\AbstractClassPass;
  15. use Psy\CodeCleaner\AssignThisVariablePass;
  16. use Psy\CodeCleaner\CalledClassPass;
  17. use Psy\CodeCleaner\CallTimePassByReferencePass;
  18. use Psy\CodeCleaner\EmptyArrayDimFetchPass;
  19. use Psy\CodeCleaner\ExitPass;
  20. use Psy\CodeCleaner\FinalClassPass;
  21. use Psy\CodeCleaner\FunctionContextPass;
  22. use Psy\CodeCleaner\FunctionReturnInWriteContextPass;
  23. use Psy\CodeCleaner\ImplicitReturnPass;
  24. use Psy\CodeCleaner\InstanceOfPass;
  25. use Psy\CodeCleaner\IssetPass;
  26. use Psy\CodeCleaner\LabelContextPass;
  27. use Psy\CodeCleaner\LeavePsyshAlonePass;
  28. use Psy\CodeCleaner\ListPass;
  29. use Psy\CodeCleaner\LoopContextPass;
  30. use Psy\CodeCleaner\MagicConstantsPass;
  31. use Psy\CodeCleaner\NamespacePass;
  32. use Psy\CodeCleaner\PassableByReferencePass;
  33. use Psy\CodeCleaner\RequirePass;
  34. use Psy\CodeCleaner\ReturnTypePass;
  35. use Psy\CodeCleaner\StrictTypesPass;
  36. use Psy\CodeCleaner\UseStatementPass;
  37. use Psy\CodeCleaner\ValidClassNamePass;
  38. use Psy\CodeCleaner\ValidConstantPass;
  39. use Psy\CodeCleaner\ValidConstructorPass;
  40. use Psy\CodeCleaner\ValidFunctionNamePass;
  41. use Psy\Exception\ParseErrorException;
  42. /**
  43. * A service to clean up user input, detect parse errors before they happen,
  44. * and generally work around issues with the PHP code evaluation experience.
  45. */
  46. class CodeCleaner
  47. {
  48. private $yolo = false;
  49. private $parser;
  50. private $printer;
  51. private $traverser;
  52. private $namespace;
  53. /**
  54. * CodeCleaner constructor.
  55. *
  56. * @param Parser|null $parser A PhpParser Parser instance. One will be created if not explicitly supplied
  57. * @param Printer|null $printer A PhpParser Printer instance. One will be created if not explicitly supplied
  58. * @param NodeTraverser|null $traverser A PhpParser NodeTraverser instance. One will be created if not explicitly supplied
  59. * @param bool $yolo run without input validation
  60. */
  61. public function __construct(Parser $parser = null, Printer $printer = null, NodeTraverser $traverser = null, $yolo = false)
  62. {
  63. $this->yolo = $yolo;
  64. if ($parser === null) {
  65. $parserFactory = new ParserFactory();
  66. $parser = $parserFactory->createParser();
  67. }
  68. $this->parser = $parser;
  69. $this->printer = $printer ?: new Printer();
  70. $this->traverser = $traverser ?: new NodeTraverser();
  71. foreach ($this->getDefaultPasses() as $pass) {
  72. $this->traverser->addVisitor($pass);
  73. }
  74. }
  75. /**
  76. * Check whether this CodeCleaner is in YOLO mode.
  77. *
  78. * @return bool
  79. */
  80. public function yolo()
  81. {
  82. return $this->yolo;
  83. }
  84. /**
  85. * Get default CodeCleaner passes.
  86. *
  87. * @return array
  88. */
  89. private function getDefaultPasses()
  90. {
  91. if ($this->yolo) {
  92. return $this->getYoloPasses();
  93. }
  94. $useStatementPass = new UseStatementPass();
  95. $namespacePass = new NamespacePass($this);
  96. // Try to add implicit `use` statements and an implicit namespace,
  97. // based on the file in which the `debug` call was made.
  98. $this->addImplicitDebugContext([$useStatementPass, $namespacePass]);
  99. return [
  100. // Validation passes
  101. new AbstractClassPass(),
  102. new AssignThisVariablePass(),
  103. new CalledClassPass(),
  104. new CallTimePassByReferencePass(),
  105. new FinalClassPass(),
  106. new FunctionContextPass(),
  107. new FunctionReturnInWriteContextPass(),
  108. new InstanceOfPass(),
  109. new IssetPass(),
  110. new LabelContextPass(),
  111. new LeavePsyshAlonePass(),
  112. new ListPass(),
  113. new LoopContextPass(),
  114. new PassableByReferencePass(),
  115. new ReturnTypePass(),
  116. new EmptyArrayDimFetchPass(),
  117. new ValidConstructorPass(),
  118. // Rewriting shenanigans
  119. $useStatementPass, // must run before the namespace pass
  120. new ExitPass(),
  121. new ImplicitReturnPass(),
  122. new MagicConstantsPass(),
  123. $namespacePass, // must run after the implicit return pass
  124. new RequirePass(),
  125. new StrictTypesPass(),
  126. // Namespace-aware validation (which depends on aforementioned shenanigans)
  127. new ValidClassNamePass(),
  128. new ValidConstantPass(),
  129. new ValidFunctionNamePass(),
  130. ];
  131. }
  132. /**
  133. * A set of code cleaner passes that don't try to do any validation, and
  134. * only do minimal rewriting to make things work inside the REPL.
  135. *
  136. * This list should stay in sync with the "rewriting shenanigans" in
  137. * getDefaultPasses above.
  138. *
  139. * @return array
  140. */
  141. private function getYoloPasses()
  142. {
  143. $useStatementPass = new UseStatementPass();
  144. $namespacePass = new NamespacePass($this);
  145. // Try to add implicit `use` statements and an implicit namespace,
  146. // based on the file in which the `debug` call was made.
  147. $this->addImplicitDebugContext([$useStatementPass, $namespacePass]);
  148. return [
  149. new LeavePsyshAlonePass(),
  150. $useStatementPass, // must run before the namespace pass
  151. new ExitPass(),
  152. new ImplicitReturnPass(),
  153. new MagicConstantsPass(),
  154. $namespacePass, // must run after the implicit return pass
  155. new RequirePass(),
  156. new StrictTypesPass(),
  157. ];
  158. }
  159. /**
  160. * "Warm up" code cleaner passes when we're coming from a debug call.
  161. *
  162. * This is useful, for example, for `UseStatementPass` and `NamespacePass`
  163. * which keep track of state between calls, to maintain the current
  164. * namespace and a map of use statements.
  165. *
  166. * @param array $passes
  167. */
  168. private function addImplicitDebugContext(array $passes)
  169. {
  170. $file = $this->getDebugFile();
  171. if ($file === null) {
  172. return;
  173. }
  174. try {
  175. $code = @\file_get_contents($file);
  176. if (!$code) {
  177. return;
  178. }
  179. $stmts = $this->parse($code, true);
  180. if ($stmts === false) {
  181. return;
  182. }
  183. // Set up a clean traverser for just these code cleaner passes
  184. $traverser = new NodeTraverser();
  185. foreach ($passes as $pass) {
  186. $traverser->addVisitor($pass);
  187. }
  188. $traverser->traverse($stmts);
  189. } catch (\Throwable $e) {
  190. // Don't care.
  191. } catch (\Exception $e) {
  192. // Still don't care.
  193. }
  194. }
  195. /**
  196. * Search the stack trace for a file in which the user called Psy\debug.
  197. *
  198. * @return string|null
  199. */
  200. private static function getDebugFile()
  201. {
  202. $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
  203. foreach (\array_reverse($trace) as $stackFrame) {
  204. if (!self::isDebugCall($stackFrame)) {
  205. continue;
  206. }
  207. if (\preg_match('/eval\(/', $stackFrame['file'])) {
  208. \preg_match_all('/([^\(]+)\((\d+)/', $stackFrame['file'], $matches);
  209. return $matches[1][0];
  210. }
  211. return $stackFrame['file'];
  212. }
  213. }
  214. /**
  215. * Check whether a given backtrace frame is a call to Psy\debug.
  216. *
  217. * @param array $stackFrame
  218. *
  219. * @return bool
  220. */
  221. private static function isDebugCall(array $stackFrame)
  222. {
  223. $class = isset($stackFrame['class']) ? $stackFrame['class'] : null;
  224. $function = isset($stackFrame['function']) ? $stackFrame['function'] : null;
  225. return ($class === null && $function === 'Psy\\debug') ||
  226. ($class === Shell::class && $function === 'debug');
  227. }
  228. /**
  229. * Clean the given array of code.
  230. *
  231. * @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP
  232. *
  233. * @param array $codeLines
  234. * @param bool $requireSemicolons
  235. *
  236. * @return string|false Cleaned PHP code, False if the input is incomplete
  237. */
  238. public function clean(array $codeLines, $requireSemicolons = false)
  239. {
  240. $stmts = $this->parse('<?php '.\implode(\PHP_EOL, $codeLines).\PHP_EOL, $requireSemicolons);
  241. if ($stmts === false) {
  242. return false;
  243. }
  244. // Catch fatal errors before they happen
  245. $stmts = $this->traverser->traverse($stmts);
  246. // Work around https://github.com/nikic/PHP-Parser/issues/399
  247. $oldLocale = \setlocale(\LC_NUMERIC, 0);
  248. \setlocale(\LC_NUMERIC, 'C');
  249. $code = $this->printer->prettyPrint($stmts);
  250. // Now put the locale back
  251. \setlocale(\LC_NUMERIC, $oldLocale);
  252. return $code;
  253. }
  254. /**
  255. * Set the current local namespace.
  256. *
  257. * @param array|null $namespace (default: null)
  258. *
  259. * @return array|null
  260. */
  261. public function setNamespace(array $namespace = null)
  262. {
  263. $this->namespace = $namespace;
  264. }
  265. /**
  266. * Get the current local namespace.
  267. *
  268. * @return array|null
  269. */
  270. public function getNamespace()
  271. {
  272. return $this->namespace;
  273. }
  274. /**
  275. * Lex and parse a block of code.
  276. *
  277. * @see Parser::parse
  278. *
  279. * @throws ParseErrorException for parse errors that can't be resolved by
  280. * waiting a line to see what comes next
  281. *
  282. * @param string $code
  283. * @param bool $requireSemicolons
  284. *
  285. * @return array|false A set of statements, or false if incomplete
  286. */
  287. protected function parse($code, $requireSemicolons = false)
  288. {
  289. try {
  290. return $this->parser->parse($code);
  291. } catch (\PhpParser\Error $e) {
  292. if ($this->parseErrorIsUnclosedString($e, $code)) {
  293. return false;
  294. }
  295. if ($this->parseErrorIsUnterminatedComment($e, $code)) {
  296. return false;
  297. }
  298. if ($this->parseErrorIsTrailingComma($e, $code)) {
  299. return false;
  300. }
  301. if (!$this->parseErrorIsEOF($e)) {
  302. throw ParseErrorException::fromParseError($e);
  303. }
  304. if ($requireSemicolons) {
  305. return false;
  306. }
  307. try {
  308. // Unexpected EOF, try again with an implicit semicolon
  309. return $this->parser->parse($code.';');
  310. } catch (\PhpParser\Error $e) {
  311. return false;
  312. }
  313. }
  314. }
  315. private function parseErrorIsEOF(\PhpParser\Error $e)
  316. {
  317. $msg = $e->getRawMessage();
  318. return ($msg === 'Unexpected token EOF') || (\strpos($msg, 'Syntax error, unexpected EOF') !== false);
  319. }
  320. /**
  321. * A special test for unclosed single-quoted strings.
  322. *
  323. * Unlike (all?) other unclosed statements, single quoted strings have
  324. * their own special beautiful snowflake syntax error just for
  325. * themselves.
  326. *
  327. * @param \PhpParser\Error $e
  328. * @param string $code
  329. *
  330. * @return bool
  331. */
  332. private function parseErrorIsUnclosedString(\PhpParser\Error $e, $code)
  333. {
  334. if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') {
  335. return false;
  336. }
  337. try {
  338. $this->parser->parse($code."';");
  339. } catch (\Exception $e) {
  340. return false;
  341. }
  342. return true;
  343. }
  344. private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code)
  345. {
  346. return $e->getRawMessage() === 'Unterminated comment';
  347. }
  348. private function parseErrorIsTrailingComma(\PhpParser\Error $e, $code)
  349. {
  350. return ($e->getRawMessage() === 'A trailing comma is not allowed here') && (\substr(\rtrim($code), -1) === ',');
  351. }
  352. }