├── LICENSE ├── README.md ├── bin └── psysh ├── composer.json └── src ├── CodeCleaner.php ├── CodeCleaner ├── AbstractClassPass.php ├── AssignThisVariablePass.php ├── CallTimePassByReferencePass.php ├── CalledClassPass.php ├── CodeCleanerPass.php ├── EmptyArrayDimFetchPass.php ├── ExitPass.php ├── FinalClassPass.php ├── FunctionContextPass.php ├── FunctionReturnInWriteContextPass.php ├── ImplicitReturnPass.php ├── IssetPass.php ├── LabelContextPass.php ├── LeavePsyshAlonePass.php ├── ListPass.php ├── LoopContextPass.php ├── MagicConstantsPass.php ├── NamespaceAwarePass.php ├── NamespacePass.php ├── NoReturnValue.php ├── PassableByReferencePass.php ├── RequirePass.php ├── ReturnTypePass.php ├── StrictTypesPass.php ├── UseStatementPass.php ├── ValidClassNamePass.php ├── ValidConstructorPass.php └── ValidFunctionNamePass.php ├── Command ├── BufferCommand.php ├── ClearCommand.php ├── CodeArgumentParser.php ├── Command.php ├── DocCommand.php ├── DumpCommand.php ├── EditCommand.php ├── ExitCommand.php ├── HelpCommand.php ├── HistoryCommand.php ├── ListCommand.php ├── ListCommand │ ├── ClassConstantEnumerator.php │ ├── ClassEnumerator.php │ ├── ConstantEnumerator.php │ ├── Enumerator.php │ ├── FunctionEnumerator.php │ ├── GlobalVariableEnumerator.php │ ├── MethodEnumerator.php │ ├── PropertyEnumerator.php │ └── VariableEnumerator.php ├── ParseCommand.php ├── PsyVersionCommand.php ├── ReflectingCommand.php ├── ShowCommand.php ├── SudoCommand.php ├── ThrowUpCommand.php ├── TimeitCommand.php ├── TimeitCommand │ └── TimeitVisitor.php ├── TraceCommand.php ├── WhereamiCommand.php └── WtfCommand.php ├── ConfigPaths.php ├── Configuration.php ├── Context.php ├── ContextAware.php ├── EnvInterface.php ├── Exception ├── BreakException.php ├── DeprecatedException.php ├── ErrorException.php ├── Exception.php ├── FatalErrorException.php ├── ParseErrorException.php ├── RuntimeException.php ├── ThrowUpException.php └── UnexpectedTargetException.php ├── ExecutionClosure.php ├── ExecutionLoop ├── AbstractListener.php ├── Listener.php ├── ProcessForker.php └── RunkitReloader.php ├── ExecutionLoopClosure.php ├── Formatter ├── CodeFormatter.php ├── DocblockFormatter.php ├── ReflectorFormatter.php ├── SignatureFormatter.php └── TraceFormatter.php ├── Input ├── CodeArgument.php ├── FilterOptions.php ├── ShellInput.php └── SilentInput.php ├── Output ├── OutputPager.php ├── PassthruPager.php ├── ProcOutputPager.php ├── ShellOutput.php └── Theme.php ├── ParserFactory.php ├── Readline ├── GNUReadline.php ├── Hoa │ ├── Autocompleter.php │ ├── AutocompleterAggregate.php │ ├── AutocompleterPath.php │ ├── AutocompleterWord.php │ ├── Console.php │ ├── ConsoleCursor.php │ ├── ConsoleException.php │ ├── ConsoleInput.php │ ├── ConsoleOutput.php │ ├── ConsoleProcessus.php │ ├── ConsoleTput.php │ ├── ConsoleWindow.php │ ├── Event.php │ ├── EventBucket.php │ ├── EventException.php │ ├── EventListenable.php │ ├── EventListener.php │ ├── EventListens.php │ ├── EventSource.php │ ├── Exception.php │ ├── ExceptionIdle.php │ ├── File.php │ ├── FileDirectory.php │ ├── FileDoesNotExistException.php │ ├── FileException.php │ ├── FileFinder.php │ ├── FileGeneric.php │ ├── FileLink.php │ ├── FileLinkRead.php │ ├── FileLinkReadWrite.php │ ├── FileRead.php │ ├── FileReadWrite.php │ ├── IStream.php │ ├── IteratorFileSystem.php │ ├── IteratorRecursiveDirectory.php │ ├── IteratorSplFileInfo.php │ ├── Protocol.php │ ├── ProtocolException.php │ ├── ProtocolNode.php │ ├── ProtocolNodeLibrary.php │ ├── ProtocolWrapper.php │ ├── Readline.php │ ├── Stream.php │ ├── StreamBufferable.php │ ├── StreamContext.php │ ├── StreamException.php │ ├── StreamIn.php │ ├── StreamLockable.php │ ├── StreamOut.php │ ├── StreamPathable.php │ ├── StreamPointable.php │ ├── StreamStatable.php │ ├── StreamTouchable.php │ ├── Terminfo │ │ ├── 77 │ │ │ └── windows-ansi │ │ └── 78 │ │ │ ├── xterm │ │ │ └── xterm-256color │ ├── Ustring.php │ └── Xcallable.php ├── Libedit.php ├── Readline.php ├── Transient.php └── Userland.php ├── Reflection ├── ReflectionConstant.php ├── ReflectionLanguageConstruct.php ├── ReflectionLanguageConstructParameter.php └── ReflectionNamespace.php ├── Shell.php ├── Sudo.php ├── Sudo └── SudoVisitor.php ├── SuperglobalsEnv.php ├── SystemEnv.php ├── TabCompletion ├── AutoCompleter.php └── Matcher │ ├── AbstractContextAwareMatcher.php │ ├── AbstractDefaultParametersMatcher.php │ ├── AbstractMatcher.php │ ├── ClassAttributesMatcher.php │ ├── ClassMethodDefaultParametersMatcher.php │ ├── ClassMethodsMatcher.php │ ├── ClassNamesMatcher.php │ ├── CommandsMatcher.php │ ├── ConstantsMatcher.php │ ├── FunctionDefaultParametersMatcher.php │ ├── FunctionsMatcher.php │ ├── KeywordsMatcher.php │ ├── MongoClientMatcher.php │ ├── MongoDatabaseMatcher.php │ ├── ObjectAttributesMatcher.php │ ├── ObjectMethodDefaultParametersMatcher.php │ ├── ObjectMethodsMatcher.php │ └── VariablesMatcher.php ├── Util ├── Docblock.php ├── Json.php ├── Mirror.php └── Str.php ├── VarDumper ├── Cloner.php ├── Dumper.php ├── Presenter.php └── PresenterAware.php ├── VersionUpdater ├── Checker.php ├── Downloader.php ├── Downloader │ ├── CurlDownloader.php │ ├── Factory.php │ └── FileDownloader.php ├── GitHubChecker.php ├── Installer.php ├── IntervalChecker.php ├── NoopChecker.php └── SelfUpdate.php └── functions.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2023 Justin Hileman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PsySH 2 | 3 | PsySH is a runtime developer console, interactive debugger and [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) for PHP. Learn more at [psysh.org](http://psysh.org/) and [in the manual](https://github.com/bobthecow/psysh/wiki/Home). 4 | 5 | 6 | [![Package version](https://img.shields.io/packagist/v/psy/psysh.svg?style=flat-square)](https://packagist.org/packages/psy/psysh) 7 | [![Monthly downloads](http://img.shields.io/packagist/dm/psy/psysh.svg?style=flat-square)](https://packagist.org/packages/psy/psysh) 8 | [![Made out of awesome](https://img.shields.io/badge/made_out_of_awesome-✓-brightgreen.svg?style=flat-square)](http://psysh.org) 9 | 10 | [![Build status](https://img.shields.io/github/actions/workflow/status/bobthecow/psysh/tests.yml?branch=main&style=flat-square)](https://github.com/bobthecow/psysh/actions?query=branch:main) 11 | [![StyleCI](https://styleci.io/repos/4549925/shield)](https://styleci.io/repos/4549925) 12 | 13 | 14 | 15 | 16 | ## [PsySH manual](https://github.com/bobthecow/psysh/wiki/Home) 17 | 18 | ### [💾 Installation](https://github.com/bobthecow/psysh/wiki/Installation) 19 | * [📕 PHP manual installation](https://github.com/bobthecow/psysh/wiki/PHP-manual) 20 | * Windows 21 | 22 | ### [🖥 Usage](https://github.com/bobthecow/psysh/wiki/Usage) 23 | * [✨ Magic variables](https://github.com/bobthecow/psysh/wiki/Magic-variables) 24 | * [⏳ Managing history](https://github.com/bobthecow/psysh/wiki/History) 25 | * [💲 System shell integration](https://github.com/bobthecow/psysh/wiki/Shell-integration) 26 | * [🎥 Tutorials & guides](https://github.com/bobthecow/psysh/wiki/Tutorials) 27 | * [🐛 Troubleshooting](https://github.com/bobthecow/psysh/wiki/Troubleshooting) 28 | 29 | ### [📢 Commands](https://github.com/bobthecow/psysh/wiki/Commands) 30 | 31 | ### [🛠 Configuration](https://github.com/bobthecow/psysh/wiki/Configuration) 32 | * [🎛 Config options](https://github.com/bobthecow/psysh/wiki/Config-options) 33 | * [🎨 Themes](https://github.com/bobthecow/psysh/wiki/Themes) 34 | * [📄 Sample config file](https://github.com/bobthecow/psysh/wiki/Sample-config) 35 | 36 | ### [🔌 Integrations](https://github.com/bobthecow/psysh/wiki/Integrations) 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "psy/psysh", 3 | "description": "An interactive shell for modern PHP.", 4 | "type": "library", 5 | "keywords": ["console", "interactive", "shell", "repl"], 6 | "homepage": "http://psysh.org", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Justin Hileman", 11 | "email": "justin@justinhileman.info", 12 | "homepage": "http://justinhileman.com" 13 | } 14 | ], 15 | "require": { 16 | "php": "^8.0 || ^7.4", 17 | "ext-json": "*", 18 | "ext-tokenizer": "*", 19 | "nikic/php-parser": "^5.0 || ^4.0", 20 | "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", 21 | "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" 22 | }, 23 | "require-dev": { 24 | "bamarni/composer-bin-plugin": "^1.2" 25 | }, 26 | "suggest": { 27 | "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", 28 | "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.", 29 | "ext-pdo-sqlite": "The doc command requires SQLite to work." 30 | }, 31 | "autoload": { 32 | "files": ["src/functions.php"], 33 | "psr-4": { 34 | "Psy\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Psy\\Test\\": "test/" 40 | } 41 | }, 42 | "bin": ["bin/psysh"], 43 | "config": { 44 | "allow-plugins": { 45 | "bamarni/composer-bin-plugin": true 46 | } 47 | }, 48 | "extra": { 49 | "branch-alias": { 50 | "dev-main": "0.12.x-dev" 51 | }, 52 | "bamarni-bin": { 53 | "bin-links": false, 54 | "forward-command": false 55 | } 56 | }, 57 | "conflict": { 58 | "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/CodeCleaner/AbstractClassPass.php: -------------------------------------------------------------------------------- 1 | class = $node; 38 | $this->abstractMethods = []; 39 | } elseif ($node instanceof ClassMethod) { 40 | if ($node->isAbstract()) { 41 | $name = \sprintf('%s::%s', $this->class->name, $node->name); 42 | $this->abstractMethods[] = $name; 43 | 44 | if ($node->stmts !== null) { 45 | $msg = \sprintf('Abstract function %s cannot contain body', $name); 46 | throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine()); 47 | } 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * @throws FatalErrorException if the node is a non-abstract class with abstract methods 54 | * 55 | * @param Node $node 56 | * 57 | * @return int|Node|Node[]|null Replacement node (or special return value) 58 | */ 59 | public function leaveNode(Node $node) 60 | { 61 | if ($node instanceof Class_) { 62 | $count = \count($this->abstractMethods); 63 | if ($count > 0 && !$node->isAbstract()) { 64 | $msg = \sprintf( 65 | 'Class %s contains %d abstract method%s must therefore be declared abstract or implement the remaining methods (%s)', 66 | $node->name, 67 | $count, 68 | ($count === 1) ? '' : 's', 69 | \implode(', ', $this->abstractMethods) 70 | ); 71 | throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine()); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/CodeCleaner/AssignThisVariablePass.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class AssignThisVariablePass extends CodeCleanerPass 25 | { 26 | /** 27 | * Validate that the user input does not assign the `$this` variable. 28 | * 29 | * @throws FatalErrorException if the user assign the `$this` variable 30 | * 31 | * @param Node $node 32 | * 33 | * @return int|Node|null Replacement node (or special return value) 34 | */ 35 | public function enterNode(Node $node) 36 | { 37 | if ($node instanceof Assign && $node->var instanceof Variable && $node->var->name === 'this') { 38 | throw new FatalErrorException('Cannot re-assign $this', 0, \E_ERROR, null, $node->getStartLine()); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/CodeCleaner/CallTimePassByReferencePass.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class CallTimePassByReferencePass extends CodeCleanerPass 29 | { 30 | const EXCEPTION_MESSAGE = 'Call-time pass-by-reference has been removed'; 31 | 32 | /** 33 | * Validate of use call-time pass-by-reference. 34 | * 35 | * @throws FatalErrorException if the user used call-time pass-by-reference 36 | * 37 | * @param Node $node 38 | * 39 | * @return int|Node|null Replacement node (or special return value) 40 | */ 41 | public function enterNode(Node $node) 42 | { 43 | if (!$node instanceof FuncCall && !$node instanceof MethodCall && !$node instanceof StaticCall) { 44 | return; 45 | } 46 | 47 | foreach ($node->args as $arg) { 48 | if ($arg instanceof VariadicPlaceholder) { 49 | continue; 50 | } 51 | 52 | if ($arg->byRef) { 53 | throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine()); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/CodeCleaner/CalledClassPass.php: -------------------------------------------------------------------------------- 1 | inClass = false; 39 | } 40 | 41 | /** 42 | * @throws ErrorException if get_class or get_called_class is called without an object from outside a class 43 | * 44 | * @param Node $node 45 | * 46 | * @return int|Node|null Replacement node (or special return value) 47 | */ 48 | public function enterNode(Node $node) 49 | { 50 | if ($node instanceof Class_ || $node instanceof Trait_) { 51 | $this->inClass = true; 52 | } elseif ($node instanceof FuncCall && !$this->inClass) { 53 | // We'll give any args at all (besides null) a pass. 54 | // Technically we should be checking whether the args are objects, but this will do for now. 55 | // 56 | // @todo switch this to actually validate args when we get context-aware code cleaner passes. 57 | if (!empty($node->args) && !$this->isNull($node->args[0])) { 58 | return; 59 | } 60 | 61 | // We'll ignore name expressions as well (things like `$foo()`) 62 | if (!($node->name instanceof Name)) { 63 | return; 64 | } 65 | 66 | $name = \strtolower($node->name); 67 | if (\in_array($name, ['get_class', 'get_called_class'])) { 68 | $msg = \sprintf('%s() called without object from outside a class', $name); 69 | throw new ErrorException($msg, 0, \E_USER_WARNING, null, $node->getStartLine()); 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * @param Node $node 76 | * 77 | * @return int|Node|Node[]|null Replacement node (or special return value) 78 | */ 79 | public function leaveNode(Node $node) 80 | { 81 | if ($node instanceof Class_) { 82 | $this->inClass = false; 83 | } 84 | } 85 | 86 | private function isNull(Node $node): bool 87 | { 88 | if ($node instanceof VariadicPlaceholder) { 89 | return false; 90 | } 91 | 92 | return $node->value instanceof ConstFetch && \strtolower($node->value->name) === 'null'; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/CodeCleaner/CodeCleanerPass.php: -------------------------------------------------------------------------------- 1 | theseOnesAreFine = []; 36 | } 37 | 38 | /** 39 | * @throws FatalErrorException if the user used empty array dim fetch outside of assignment 40 | * 41 | * @param Node $node 42 | * 43 | * @return int|Node|null Replacement node (or special return value) 44 | */ 45 | public function enterNode(Node $node) 46 | { 47 | if ($node instanceof Assign && $node->var instanceof ArrayDimFetch) { 48 | $this->theseOnesAreFine[] = $node->var; 49 | } elseif ($node instanceof AssignRef && $node->expr instanceof ArrayDimFetch) { 50 | $this->theseOnesAreFine[] = $node->expr; 51 | } elseif ($node instanceof Foreach_ && $node->valueVar instanceof ArrayDimFetch) { 52 | $this->theseOnesAreFine[] = $node->valueVar; 53 | } elseif ($node instanceof ArrayDimFetch && $node->var instanceof ArrayDimFetch) { 54 | // $a[]['b'] = 'c' 55 | if (\in_array($node, $this->theseOnesAreFine)) { 56 | $this->theseOnesAreFine[] = $node->var; 57 | } 58 | } 59 | 60 | if ($node instanceof ArrayDimFetch && $node->dim === null) { 61 | if (!\in_array($node, $this->theseOnesAreFine)) { 62 | throw new FatalErrorException(self::EXCEPTION_MESSAGE, $node->getStartLine()); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/CodeCleaner/ExitPass.php: -------------------------------------------------------------------------------- 1 | finalClasses = []; 33 | } 34 | 35 | /** 36 | * @throws FatalErrorException if the node is a class that extends a final class 37 | * 38 | * @param Node $node 39 | * 40 | * @return int|Node|null Replacement node (or special return value) 41 | */ 42 | public function enterNode(Node $node) 43 | { 44 | if ($node instanceof Class_) { 45 | if ($node->extends) { 46 | $extends = (string) $node->extends; 47 | if ($this->isFinalClass($extends)) { 48 | $msg = \sprintf('Class %s may not inherit from final class (%s)', $node->name, $extends); 49 | throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine()); 50 | } 51 | } 52 | 53 | if ($node->isFinal()) { 54 | $this->finalClasses[\strtolower($node->name)] = true; 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * @param string $name Class name 61 | */ 62 | private function isFinalClass(string $name): bool 63 | { 64 | if (!\class_exists($name)) { 65 | return isset($this->finalClasses[\strtolower($name)]); 66 | } 67 | 68 | $refl = new \ReflectionClass($name); 69 | 70 | return $refl->isFinal(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/CodeCleaner/FunctionContextPass.php: -------------------------------------------------------------------------------- 1 | functionDepth = 0; 31 | } 32 | 33 | /** 34 | * @return int|Node|null Replacement node (or special return value) 35 | */ 36 | public function enterNode(Node $node) 37 | { 38 | if ($node instanceof FunctionLike) { 39 | $this->functionDepth++; 40 | 41 | return; 42 | } 43 | 44 | // node is inside function context 45 | if ($this->functionDepth !== 0) { 46 | return; 47 | } 48 | 49 | // It causes fatal error. 50 | if ($node instanceof Yield_) { 51 | $msg = 'The "yield" expression can only be used inside a function'; 52 | throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine()); 53 | } 54 | } 55 | 56 | /** 57 | * @param \PhpParser\Node $node 58 | * 59 | * @return int|Node|Node[]|null Replacement node (or special return value) 60 | */ 61 | public function leaveNode(Node $node) 62 | { 63 | if ($node instanceof FunctionLike) { 64 | $this->functionDepth--; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/CodeCleaner/FunctionReturnInWriteContextPass.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class FunctionReturnInWriteContextPass extends CodeCleanerPass 31 | { 32 | const ISSET_MESSAGE = 'Cannot use isset() on the result of an expression (you can use "null !== expression" instead)'; 33 | const EXCEPTION_MESSAGE = "Can't use function return value in write context"; 34 | 35 | /** 36 | * Validate that the functions are used correctly. 37 | * 38 | * @throws FatalErrorException if a function is passed as an argument reference 39 | * @throws FatalErrorException if a function is used as an argument in the isset 40 | * @throws FatalErrorException if a value is assigned to a function 41 | * 42 | * @param Node $node 43 | * 44 | * @return int|Node|null Replacement node (or special return value) 45 | */ 46 | public function enterNode(Node $node) 47 | { 48 | if ($node instanceof Array_ || $this->isCallNode($node)) { 49 | $items = $node instanceof Array_ ? $node->items : $node->args; 50 | foreach ($items as $item) { 51 | if ($item instanceof VariadicPlaceholder) { 52 | continue; 53 | } 54 | 55 | if ($item && $item->byRef && $this->isCallNode($item->value)) { 56 | throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine()); 57 | } 58 | } 59 | } elseif ($node instanceof Isset_ || $node instanceof Unset_) { 60 | foreach ($node->vars as $var) { 61 | if (!$this->isCallNode($var)) { 62 | continue; 63 | } 64 | 65 | $msg = $node instanceof Isset_ ? self::ISSET_MESSAGE : self::EXCEPTION_MESSAGE; 66 | throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine()); 67 | } 68 | } elseif ($node instanceof Assign && $this->isCallNode($node->var)) { 69 | throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine()); 70 | } 71 | } 72 | 73 | private function isCallNode(Node $node): bool 74 | { 75 | return $node instanceof FuncCall || $node instanceof MethodCall || $node instanceof StaticCall; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/CodeCleaner/IssetPass.php: -------------------------------------------------------------------------------- 1 | vars as $var) { 44 | if (!$var instanceof Variable && !$var instanceof ArrayDimFetch && !$var instanceof PropertyFetch && !$var instanceof NullsafePropertyFetch) { 45 | throw new FatalErrorException(self::EXCEPTION_MSG, 0, \E_ERROR, null, $node->getStartLine()); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/CodeCleaner/LabelContextPass.php: -------------------------------------------------------------------------------- 1 | functionDepth = 0; 46 | $this->labelDeclarations = []; 47 | $this->labelGotos = []; 48 | } 49 | 50 | /** 51 | * @return int|Node|null Replacement node (or special return value) 52 | */ 53 | public function enterNode(Node $node) 54 | { 55 | if ($node instanceof FunctionLike) { 56 | $this->functionDepth++; 57 | 58 | return; 59 | } 60 | 61 | // node is inside function context 62 | if ($this->functionDepth !== 0) { 63 | return; 64 | } 65 | 66 | if ($node instanceof Goto_) { 67 | $this->labelGotos[\strtolower($node->name)] = $node->getStartLine(); 68 | } elseif ($node instanceof Label) { 69 | $this->labelDeclarations[\strtolower($node->name)] = $node->getStartLine(); 70 | } 71 | } 72 | 73 | /** 74 | * @param \PhpParser\Node $node 75 | * 76 | * @return int|Node|Node[]|null Replacement node (or special return value) 77 | */ 78 | public function leaveNode(Node $node) 79 | { 80 | if ($node instanceof FunctionLike) { 81 | $this->functionDepth--; 82 | } 83 | } 84 | 85 | /** 86 | * @return Node[]|null Array of nodes 87 | */ 88 | public function afterTraverse(array $nodes) 89 | { 90 | foreach ($this->labelGotos as $name => $line) { 91 | if (!isset($this->labelDeclarations[$name])) { 92 | $msg = "'goto' to undefined label '{$name}'"; 93 | throw new FatalErrorException($msg, 0, \E_ERROR, null, $line); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/CodeCleaner/LeavePsyshAlonePass.php: -------------------------------------------------------------------------------- 1 | name === '__psysh__') { 35 | throw new RuntimeException('Don\'t mess with $__psysh__; bad things will happen'); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/CodeCleaner/ListPass.php: -------------------------------------------------------------------------------- 1 | var instanceof Array_ && !$node->var instanceof List_) { 49 | return; 50 | } 51 | 52 | // Polyfill for PHP-Parser 2.x 53 | $items = isset($node->var->items) ? $node->var->items : $node->var->vars; 54 | 55 | if ($items === [] || $items === [null]) { 56 | throw new ParseErrorException('Cannot use empty list', ['startLine' => $node->var->getStartLine(), 'endLine' => $node->var->getEndLine()]); 57 | } 58 | 59 | $itemFound = false; 60 | foreach ($items as $item) { 61 | if ($item === null) { 62 | continue; 63 | } 64 | 65 | $itemFound = true; 66 | 67 | if (!self::isValidArrayItem($item)) { 68 | $msg = 'Assignments can only happen to writable values'; 69 | throw new ParseErrorException($msg, ['startLine' => $item->getStartLine(), 'endLine' => $item->getEndLine()]); 70 | } 71 | } 72 | 73 | if (!$itemFound) { 74 | throw new ParseErrorException('Cannot use empty list'); 75 | } 76 | } 77 | 78 | /** 79 | * Validate whether a given item in an array is valid for short assignment. 80 | * 81 | * @param Node $item 82 | */ 83 | private static function isValidArrayItem(Node $item): bool 84 | { 85 | $value = ($item instanceof ArrayItem || $item instanceof LegacyArrayItem) ? $item->value : $item; 86 | 87 | while ($value instanceof ArrayDimFetch || $value instanceof PropertyFetch) { 88 | $value = $value->var; 89 | } 90 | 91 | // We just kind of give up if it's a method call. We can't tell if it's 92 | // valid via static analysis. 93 | return $value instanceof Variable || $value instanceof MethodCall || $value instanceof FuncCall; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/CodeCleaner/MagicConstantsPass.php: -------------------------------------------------------------------------------- 1 | getAttributes()); 38 | } elseif ($node instanceof File) { 39 | return new String_('', $node->getAttributes()); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/CodeCleaner/NamespaceAwarePass.php: -------------------------------------------------------------------------------- 1 | namespace = []; 38 | $this->currentScope = []; 39 | } 40 | 41 | /** 42 | * @todo should this be final? Extending classes should be sure to either use 43 | * leaveNode or call parent::enterNode() when overloading 44 | * 45 | * @param Node $node 46 | * 47 | * @return int|Node|null Replacement node (or special return value) 48 | */ 49 | public function enterNode(Node $node) 50 | { 51 | if ($node instanceof Namespace_) { 52 | $this->namespace = isset($node->name) ? $this->getParts($node->name) : []; 53 | } 54 | } 55 | 56 | /** 57 | * Get a fully-qualified name (class, function, interface, etc). 58 | * 59 | * @param mixed $name 60 | */ 61 | protected function getFullyQualifiedName($name): string 62 | { 63 | if ($name instanceof FullyQualifiedName) { 64 | return \implode('\\', $this->getParts($name)); 65 | } 66 | 67 | if ($name instanceof Name) { 68 | $name = $this->getParts($name); 69 | } elseif (!\is_array($name)) { 70 | $name = [$name]; 71 | } 72 | 73 | return \implode('\\', \array_merge($this->namespace, $name)); 74 | } 75 | 76 | /** 77 | * Backwards compatibility shim for PHP-Parser 4.x. 78 | * 79 | * At some point we might want to make $namespace a plain string, to match how Name works? 80 | */ 81 | protected function getParts(Name $name): array 82 | { 83 | return \method_exists($name, 'getParts') ? $name->getParts() : $name->parts; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/CodeCleaner/NamespacePass.php: -------------------------------------------------------------------------------- 1 | cleaner = $cleaner; 41 | } 42 | 43 | /** 44 | * If this is a standalone namespace line, remember it for later. 45 | * 46 | * Otherwise, apply remembered namespaces to the code until a new namespace 47 | * is encountered. 48 | * 49 | * @param array $nodes 50 | * 51 | * @return Node[]|null Array of nodes 52 | */ 53 | public function beforeTraverse(array $nodes) 54 | { 55 | if (empty($nodes)) { 56 | return $nodes; 57 | } 58 | 59 | $last = \end($nodes); 60 | 61 | if ($last instanceof Namespace_) { 62 | $kind = $last->getAttribute('kind'); 63 | 64 | // Treat all namespace statements pre-PHP-Parser v3.1.2 as "open", 65 | // even though we really have no way of knowing. 66 | if ($kind === null || $kind === Namespace_::KIND_SEMICOLON) { 67 | // Save the current namespace for open namespaces 68 | $this->setNamespace($last->name); 69 | } else { 70 | // Clear the current namespace after a braced namespace 71 | $this->setNamespace(null); 72 | } 73 | 74 | return $nodes; 75 | } 76 | 77 | return $this->namespace ? [new Namespace_($this->namespace, $nodes)] : $nodes; 78 | } 79 | 80 | /** 81 | * Remember the namespace and (re)set the namespace on the CodeCleaner as 82 | * well. 83 | * 84 | * @param Name|null $namespace 85 | */ 86 | private function setNamespace(?Name $namespace) 87 | { 88 | $this->namespace = $namespace; 89 | $this->cleaner->setNamespace($namespace === null ? null : $this->getParts($namespace)); 90 | } 91 | 92 | /** 93 | * Backwards compatibility shim for PHP-Parser 4.x. 94 | * 95 | * At some point we might want to make the namespace a plain string, to match how Name works? 96 | */ 97 | protected function getParts(Name $name): array 98 | { 99 | return \method_exists($name, 'getParts') ? $name->getParts() : $name->parts; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/CodeCleaner/NoReturnValue.php: -------------------------------------------------------------------------------- 1 | strictTypes = $strictTypes; 44 | } 45 | 46 | /** 47 | * If this is a standalone strict types declaration, remember it for later. 48 | * 49 | * Otherwise, apply remembered strict types declaration to to the code until 50 | * a new declaration is encountered. 51 | * 52 | * @throws FatalErrorException if an invalid `strict_types` declaration is found 53 | * 54 | * @param array $nodes 55 | * 56 | * @return Node[]|null Array of nodes 57 | */ 58 | public function beforeTraverse(array $nodes) 59 | { 60 | $prependStrictTypes = $this->strictTypes; 61 | 62 | foreach ($nodes as $node) { 63 | if ($node instanceof Declare_) { 64 | foreach ($node->declares as $declare) { 65 | if ($declare->key->toString() === 'strict_types') { 66 | $value = $declare->value; 67 | // @todo Remove LNumber once we drop support for PHP-Parser 4.x 68 | if ((!$value instanceof LNumber && !$value instanceof Int_) || ($value->value !== 0 && $value->value !== 1)) { 69 | throw new FatalErrorException(self::EXCEPTION_MESSAGE, 0, \E_ERROR, null, $node->getStartLine()); 70 | } 71 | 72 | $this->strictTypes = $value->value === 1; 73 | } 74 | } 75 | } 76 | } 77 | 78 | if ($prependStrictTypes) { 79 | $first = \reset($nodes); 80 | if (!$first instanceof Declare_) { 81 | // @todo Switch to PhpParser\Node\DeclareItem once we drop support for PHP-Parser 4.x 82 | // @todo Remove LNumber once we drop support for PHP-Parser 4.x 83 | $arg = \class_exists('PhpParser\Node\Scalar\Int_') ? new Int_(1) : new LNumber(1); 84 | $declareItem = \class_exists('PhpParser\Node\DeclareItem') ? 85 | new DeclareItem('strict_types', $arg) : 86 | new DeclareDeclare('strict_types', $arg); 87 | $declare = new Declare_([$declareItem]); 88 | \array_unshift($nodes, $declare); 89 | } 90 | } 91 | 92 | return $nodes; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/CodeCleaner/ValidFunctionNamePass.php: -------------------------------------------------------------------------------- 1 | conditionalScopes++; 47 | } elseif ($node instanceof Function_) { 48 | $name = $this->getFullyQualifiedName($node->name); 49 | 50 | // @todo add an "else" here which adds a runtime check for instances where we can't tell 51 | // whether a function is being redefined by static analysis alone. 52 | if ($this->conditionalScopes === 0) { 53 | if (\function_exists($name) || 54 | isset($this->currentScope[\strtolower($name)])) { 55 | $msg = \sprintf('Cannot redeclare %s()', $name); 56 | throw new FatalErrorException($msg, 0, \E_ERROR, null, $node->getStartLine()); 57 | } 58 | } 59 | 60 | $this->currentScope[\strtolower($name)] = true; 61 | } 62 | } 63 | 64 | /** 65 | * @param Node $node 66 | * 67 | * @return int|Node|Node[]|null Replacement node (or special return value) 68 | */ 69 | public function leaveNode(Node $node) 70 | { 71 | if (self::isConditional($node)) { 72 | $this->conditionalScopes--; 73 | } 74 | } 75 | 76 | private static function isConditional(Node $node) 77 | { 78 | return $node instanceof If_ || 79 | $node instanceof While_ || 80 | $node instanceof Do_ || 81 | $node instanceof Switch_; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Command/BufferCommand.php: -------------------------------------------------------------------------------- 1 | setName('buffer') 33 | ->setAliases(['buf']) 34 | ->setDefinition([ 35 | new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the current buffer.'), 36 | ]) 37 | ->setDescription('Show (or clear) the contents of the code input buffer.') 38 | ->setHelp( 39 | <<<'HELP' 40 | Show the contents of the code buffer for the current multi-line expression. 41 | 42 | Optionally, clear the buffer by passing the --clear option. 43 | HELP 44 | ); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | * 50 | * @return int 0 if everything went fine, or an exit code 51 | */ 52 | protected function execute(InputInterface $input, OutputInterface $output): int 53 | { 54 | $shell = $this->getShell(); 55 | 56 | $buf = $shell->getCodeBuffer(); 57 | if ($input->getOption('clear')) { 58 | $shell->resetCodeBuffer(); 59 | $output->writeln($this->formatLines($buf, 'urgent'), ShellOutput::NUMBER_LINES); 60 | } else { 61 | $output->writeln($this->formatLines($buf), ShellOutput::NUMBER_LINES); 62 | } 63 | 64 | return 0; 65 | } 66 | 67 | /** 68 | * A helper method for wrapping buffer lines in `` and `` formatter strings. 69 | * 70 | * @param array $lines 71 | * @param string $type (default: 'return') 72 | * 73 | * @return array Formatted strings 74 | */ 75 | protected function formatLines(array $lines, string $type = 'return'): array 76 | { 77 | $template = \sprintf('<%s>%%s', $type, $type); 78 | 79 | return \array_map(function ($line) use ($template) { 80 | return \sprintf($template, $line); 81 | }, $lines); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Command/ClearCommand.php: -------------------------------------------------------------------------------- 1 | setName('clear') 31 | ->setDefinition([]) 32 | ->setDescription('Clear the Psy Shell screen.') 33 | ->setHelp( 34 | <<<'HELP' 35 | Clear the Psy Shell screen. 36 | 37 | Pro Tip: If your PHP has readline support, you should be able to use ctrl+l too! 38 | HELP 39 | ); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | * 45 | * @return int 0 if everything went fine, or an exit code 46 | */ 47 | protected function execute(InputInterface $input, OutputInterface $output): int 48 | { 49 | $output->write(\sprintf('%c[2J%c[0;0f', 27, 27)); 50 | 51 | return 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Command/CodeArgumentParser.php: -------------------------------------------------------------------------------- 1 | parser = $parser ?? (new ParserFactory())->createParser(); 28 | } 29 | 30 | /** 31 | * Lex and parse a string of code into statements. 32 | * 33 | * This is intended for code arguments, so the code string *should not* start with parser->parse($code); 45 | } catch (\PhpParser\Error $e) { 46 | if (\strpos($e->getMessage(), 'unexpected EOF') === false) { 47 | throw ParseErrorException::fromParseError($e); 48 | } 49 | 50 | // If we got an unexpected EOF, let's try it again with a semicolon. 51 | try { 52 | return $this->parser->parse($code.';'); 53 | } catch (\PhpParser\Error $_e) { 54 | // Throw the original error, not the semicolon one. 55 | throw ParseErrorException::fromParseError($e); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Command/DumpCommand.php: -------------------------------------------------------------------------------- 1 | presenter = $presenter; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | protected function configure() 46 | { 47 | $this 48 | ->setName('dump') 49 | ->setDefinition([ 50 | new CodeArgument('target', CodeArgument::REQUIRED, 'A target object or primitive to dump.'), 51 | new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse.', 10), 52 | new InputOption('all', 'a', InputOption::VALUE_NONE, 'Include private and protected methods and properties.'), 53 | ]) 54 | ->setDescription('Dump an object or primitive.') 55 | ->setHelp( 56 | <<<'HELP' 57 | Dump an object or primitive. 58 | 59 | This is like var_dump but way awesomer. 60 | 61 | e.g. 62 | >>> dump $_ 63 | >>> dump $someVar 64 | >>> dump $stuff->getAll() 65 | HELP 66 | ); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | * 72 | * @return int 0 if everything went fine, or an exit code 73 | */ 74 | protected function execute(InputInterface $input, OutputInterface $output): int 75 | { 76 | if (!$output instanceof ShellOutput) { 77 | throw new RuntimeException('DumpCommand requires a ShellOutput'); 78 | } 79 | 80 | $depth = $input->getOption('depth'); 81 | $target = $this->resolveCode($input->getArgument('target')); 82 | $output->page($this->presenter->present($target, $depth, $input->getOption('all') ? Presenter::VERBOSE : 0)); 83 | 84 | if (\is_object($target)) { 85 | $this->setCommandScopeVariables(new \ReflectionObject($target)); 86 | } 87 | 88 | return 0; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Command/ExitCommand.php: -------------------------------------------------------------------------------- 1 | setName('exit') 32 | ->setAliases(['quit', 'q']) 33 | ->setDefinition([]) 34 | ->setDescription('End the current session and return to caller.') 35 | ->setHelp( 36 | <<<'HELP' 37 | End the current session and return to caller. 38 | 39 | e.g. 40 | >>> exit 41 | HELP 42 | ); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | * 48 | * @return int 0 if everything went fine, or an exit code 49 | */ 50 | protected function execute(InputInterface $input, OutputInterface $output): int 51 | { 52 | throw new BreakException('Goodbye'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Command/HelpCommand.php: -------------------------------------------------------------------------------- 1 | setName('help') 35 | ->setAliases(['?']) 36 | ->setDefinition([ 37 | new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name.', null), 38 | ]) 39 | ->setDescription('Show a list of commands. Type `help [foo]` for information about [foo].') 40 | ->setHelp('My. How meta.'); 41 | } 42 | 43 | /** 44 | * Helper for setting a subcommand to retrieve help for. 45 | * 46 | * @param Command $command 47 | */ 48 | public function setCommand(Command $command) 49 | { 50 | $this->command = $command; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | * 56 | * @return int 0 if everything went fine, or an exit code 57 | */ 58 | protected function execute(InputInterface $input, OutputInterface $output): int 59 | { 60 | if ($this->command !== null) { 61 | // help for an individual command 62 | $output->page($this->command->asText()); 63 | $this->command = null; 64 | } elseif ($name = $input->getArgument('command_name')) { 65 | // help for an individual command 66 | $output->page($this->getApplication()->get($name)->asText()); 67 | } else { 68 | // list available commands 69 | $commands = $this->getApplication()->all(); 70 | 71 | $table = $this->getTable($output); 72 | 73 | foreach ($commands as $name => $command) { 74 | if ($name !== $command->getName()) { 75 | continue; 76 | } 77 | 78 | if ($command->getAliases()) { 79 | $aliases = \sprintf('Aliases: %s', \implode(', ', $command->getAliases())); 80 | } else { 81 | $aliases = ''; 82 | } 83 | 84 | $table->addRow([ 85 | \sprintf('%s', $name), 86 | $command->getDescription(), 87 | $aliases, 88 | ]); 89 | } 90 | 91 | if ($output instanceof ShellOutput) { 92 | $output->startPaging(); 93 | } 94 | 95 | $table->render(); 96 | 97 | if ($output instanceof ShellOutput) { 98 | $output->stopPaging(); 99 | } 100 | } 101 | 102 | return 0; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Command/ListCommand/Enumerator.php: -------------------------------------------------------------------------------- 1 | filter = new FilterOptions(); 45 | $this->presenter = $presenter; 46 | } 47 | 48 | /** 49 | * Return a list of categorized things with the given input options and target. 50 | * 51 | * @param InputInterface $input 52 | * @param \Reflector|null $reflector 53 | * @param mixed $target 54 | * 55 | * @return array 56 | */ 57 | public function enumerate(InputInterface $input, ?\Reflector $reflector = null, $target = null): array 58 | { 59 | $this->filter->bind($input); 60 | 61 | return $this->listItems($input, $reflector, $target); 62 | } 63 | 64 | /** 65 | * Enumerate specific items with the given input options and target. 66 | * 67 | * Implementing classes should return an array of arrays: 68 | * 69 | * [ 70 | * 'Constants' => [ 71 | * 'FOO' => [ 72 | * 'name' => 'FOO', 73 | * 'style' => 'public', 74 | * 'value' => '123', 75 | * ], 76 | * ], 77 | * ] 78 | * 79 | * @param InputInterface $input 80 | * @param \Reflector|null $reflector 81 | * @param mixed $target 82 | * 83 | * @return array 84 | */ 85 | abstract protected function listItems(InputInterface $input, ?\Reflector $reflector = null, $target = null): array; 86 | 87 | protected function showItem($name) 88 | { 89 | return $this->filter->match($name); 90 | } 91 | 92 | protected function presentRef($value) 93 | { 94 | return $this->presenter->presentRef($value); 95 | } 96 | 97 | protected function presentSignature($target) 98 | { 99 | // This might get weird if the signature is actually for a reflector. Hrm. 100 | if (!$target instanceof \Reflector) { 101 | $target = Mirror::get($target); 102 | } 103 | 104 | return SignatureFormatter::format($target); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Command/ListCommand/GlobalVariableEnumerator.php: -------------------------------------------------------------------------------- 1 | getOption('globals')) { 33 | return []; 34 | } 35 | 36 | $globals = $this->prepareGlobals($this->getGlobals()); 37 | 38 | if (empty($globals)) { 39 | return []; 40 | } 41 | 42 | return [ 43 | 'Global Variables' => $globals, 44 | ]; 45 | } 46 | 47 | /** 48 | * Get defined global variables. 49 | * 50 | * @return array 51 | */ 52 | protected function getGlobals(): array 53 | { 54 | global $GLOBALS; 55 | 56 | $names = \array_keys($GLOBALS); 57 | \natcasesort($names); 58 | 59 | $ret = []; 60 | foreach ($names as $name) { 61 | $ret[$name] = $GLOBALS[$name]; 62 | } 63 | 64 | return $ret; 65 | } 66 | 67 | /** 68 | * Prepare formatted global variable array. 69 | * 70 | * @param array $globals 71 | * 72 | * @return array 73 | */ 74 | protected function prepareGlobals(array $globals): array 75 | { 76 | // My kingdom for a generator. 77 | $ret = []; 78 | 79 | foreach ($globals as $name => $value) { 80 | if ($this->showItem($name)) { 81 | $fname = '$'.$name; 82 | $ret[$fname] = [ 83 | 'name' => $fname, 84 | 'style' => self::IS_GLOBAL, 85 | 'value' => $this->presentRef($value), 86 | ]; 87 | } 88 | } 89 | 90 | return $ret; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Command/PsyVersionCommand.php: -------------------------------------------------------------------------------- 1 | setName('version') 29 | ->setDefinition([]) 30 | ->setDescription('Show Psy Shell version.') 31 | ->setHelp('Show Psy Shell version.'); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function execute(InputInterface $input, OutputInterface $output): int 38 | { 39 | $output->writeln($this->getApplication()->getVersion()); 40 | 41 | return 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Command/TraceCommand.php: -------------------------------------------------------------------------------- 1 | filter = new FilterOptions(); 34 | 35 | parent::__construct($name); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function configure() 42 | { 43 | list($grep, $insensitive, $invert) = FilterOptions::getOptions(); 44 | 45 | $this 46 | ->setName('trace') 47 | ->setDefinition([ 48 | new InputOption('include-psy', 'p', InputOption::VALUE_NONE, 'Include Psy in the call stack.'), 49 | new InputOption('num', 'n', InputOption::VALUE_REQUIRED, 'Only include NUM lines.'), 50 | 51 | $grep, 52 | $insensitive, 53 | $invert, 54 | ]) 55 | ->setDescription('Show the current call stack.') 56 | ->setHelp( 57 | <<<'HELP' 58 | Show the current call stack. 59 | 60 | Optionally, include PsySH in the call stack by passing the --include-psy option. 61 | 62 | e.g. 63 | > trace -n10 64 | > trace --include-psy 65 | HELP 66 | ); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | * 72 | * @return int 0 if everything went fine, or an exit code 73 | */ 74 | protected function execute(InputInterface $input, OutputInterface $output): int 75 | { 76 | $this->filter->bind($input); 77 | $trace = $this->getBacktrace(new \Exception(), $input->getOption('num'), $input->getOption('include-psy')); 78 | $output->page($trace, ShellOutput::NUMBER_LINES); 79 | 80 | return 0; 81 | } 82 | 83 | /** 84 | * Get a backtrace for an exception or error. 85 | * 86 | * Optionally limit the number of rows to include with $count, and exclude 87 | * Psy from the trace. 88 | * 89 | * @param \Throwable $e The exception or error with a backtrace 90 | * @param int $count (default: PHP_INT_MAX) 91 | * @param bool $includePsy (default: true) 92 | * 93 | * @return array Formatted stacktrace lines 94 | */ 95 | protected function getBacktrace(\Throwable $e, ?int $count = null, bool $includePsy = true): array 96 | { 97 | return TraceFormatter::formatTrace($e, $this->filter, $count, $includePsy); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/ContextAware.php: -------------------------------------------------------------------------------- 1 | rawMessage = $message; 27 | parent::__construct(\sprintf('Exit: %s', $message), $code, $previous); 28 | } 29 | 30 | /** 31 | * Return a raw (unformatted) version of the error message. 32 | */ 33 | public function getRawMessage(): string 34 | { 35 | return $this->rawMessage; 36 | } 37 | 38 | /** 39 | * Throws BreakException. 40 | * 41 | * Since `throw` can not be inserted into arbitrary expressions, it wraps with function call. 42 | * 43 | * @throws BreakException 44 | */ 45 | public static function exitShell() 46 | { 47 | throw new self('Goodbye'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Exception/DeprecatedException.php: -------------------------------------------------------------------------------- 1 | rawMessage = $message; 39 | $message = \sprintf('PHP Fatal error: %s in %s on line %d', $message, $filename ?: "eval()'d code", $lineno); 40 | parent::__construct($message, $code, $severity, $filename, $lineno, $previous); 41 | } 42 | 43 | /** 44 | * Return a raw (unformatted) version of the error message. 45 | */ 46 | public function getRawMessage(): string 47 | { 48 | return $this->rawMessage; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Exception/ParseErrorException.php: -------------------------------------------------------------------------------- 1 | $attributes]; 32 | } 33 | 34 | parent::__construct($message, $attributes); 35 | } 36 | 37 | /** 38 | * Create a ParseErrorException from a PhpParser Error. 39 | * 40 | * @param \PhpParser\Error $e 41 | */ 42 | public static function fromParseError(\PhpParser\Error $e): self 43 | { 44 | return new self($e->getRawMessage(), $e->getAttributes()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | rawMessage = $message; 31 | parent::__construct($message, $code, $previous); 32 | } 33 | 34 | /** 35 | * Return a raw (unformatted) version of the error message. 36 | */ 37 | public function getRawMessage(): string 38 | { 39 | return $this->rawMessage; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Exception/ThrowUpException.php: -------------------------------------------------------------------------------- 1 | getMessage()); 25 | parent::__construct($message, $throwable->getCode(), $throwable); 26 | } 27 | 28 | /** 29 | * Return a raw (unformatted) version of the error message. 30 | */ 31 | public function getRawMessage(): string 32 | { 33 | return $this->getPrevious()->getMessage(); 34 | } 35 | 36 | /** 37 | * Create a ThrowUpException from a Throwable. 38 | * 39 | * @deprecated PsySH no longer wraps Throwables 40 | * 41 | * @param \Throwable $throwable 42 | */ 43 | public static function fromThrowable($throwable) 44 | { 45 | @\trigger_error('PsySH no longer wraps Throwables', \E_USER_DEPRECATED); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Exception/UnexpectedTargetException.php: -------------------------------------------------------------------------------- 1 | target = $target; 28 | parent::__construct($message, $code, $previous); 29 | } 30 | 31 | /** 32 | * @return mixed 33 | */ 34 | public function getTarget() 35 | { 36 | return $this->target; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ExecutionClosure.php: -------------------------------------------------------------------------------- 1 | setClosure($__psysh__, function () use ($__psysh__) { 29 | try { 30 | // Restore execution scope variables 31 | \extract($__psysh__->getScopeVariables(false)); 32 | 33 | // Buffer stdout; we'll need it later 34 | \ob_start([$__psysh__, 'writeStdout'], 1); 35 | 36 | // Convert all errors to exceptions 37 | \set_error_handler([$__psysh__, 'handleError']); 38 | 39 | // Evaluate the current code buffer 40 | $_ = eval($__psysh__->onExecute($__psysh__->flushCode() ?: self::NOOP_INPUT)); 41 | } catch (\Throwable $_e) { 42 | // Clean up on our way out. 43 | if (\ob_get_level() > 0) { 44 | \ob_end_clean(); 45 | } 46 | 47 | throw $_e; 48 | } finally { 49 | // Won't be needing this anymore 50 | \restore_error_handler(); 51 | } 52 | 53 | // Flush stdout (write to shell output, plus save to magic variable) 54 | \ob_end_flush(); 55 | 56 | // Save execution scope variables for next time 57 | $__psysh__->setScopeVariables(\get_defined_vars()); 58 | 59 | return $_; 60 | }); 61 | } 62 | 63 | /** 64 | * Set the closure instance. 65 | * 66 | * @param Shell $shell 67 | * @param \Closure $closure 68 | */ 69 | protected function setClosure(Shell $shell, \Closure $closure) 70 | { 71 | $that = $shell->getBoundObject(); 72 | 73 | if (\is_object($that)) { 74 | $this->closure = $closure->bindTo($that, \get_class($that)); 75 | } else { 76 | $this->closure = $closure->bindTo(null, $shell->getBoundClass()); 77 | } 78 | } 79 | 80 | /** 81 | * Go go gadget closure. 82 | * 83 | * @return mixed 84 | */ 85 | public function execute() 86 | { 87 | $closure = $this->closure; 88 | 89 | return $closure(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/ExecutionLoop/AbstractListener.php: -------------------------------------------------------------------------------- 1 | setClosure($__psysh__, function () use ($__psysh__) { 30 | // Restore execution scope variables 31 | \extract($__psysh__->getScopeVariables(false)); 32 | 33 | while (true) { 34 | $__psysh__->beforeLoop(); 35 | 36 | try { 37 | $__psysh__->getInput(); 38 | 39 | try { 40 | // Pull in any new execution scope variables 41 | if ($__psysh__->getLastExecSuccess()) { 42 | \extract($__psysh__->getScopeVariablesDiff(\get_defined_vars())); 43 | } 44 | 45 | // Buffer stdout; we'll need it later 46 | \ob_start([$__psysh__, 'writeStdout'], 1); 47 | 48 | // Convert all errors to exceptions 49 | \set_error_handler([$__psysh__, 'handleError']); 50 | 51 | // Evaluate the current code buffer 52 | $_ = eval($__psysh__->onExecute($__psysh__->flushCode() ?: ExecutionClosure::NOOP_INPUT)); 53 | } catch (\Throwable $_e) { 54 | // Clean up on our way out. 55 | if (\ob_get_level() > 0) { 56 | \ob_end_clean(); 57 | } 58 | 59 | throw $_e; 60 | } finally { 61 | // Won't be needing this anymore 62 | \restore_error_handler(); 63 | } 64 | 65 | // Flush stdout (write to shell output, plus save to magic variable) 66 | \ob_end_flush(); 67 | 68 | // Save execution scope variables for next time 69 | $__psysh__->setScopeVariables(\get_defined_vars()); 70 | 71 | $__psysh__->writeReturnValue($_); 72 | } catch (BreakException $_e) { 73 | $__psysh__->writeException($_e); 74 | 75 | return; 76 | } catch (ThrowUpException $_e) { 77 | $__psysh__->writeException($_e); 78 | 79 | throw $_e; 80 | } catch (\Throwable $_e) { 81 | $__psysh__->writeException($_e); 82 | } 83 | 84 | $__psysh__->afterLoop(); 85 | } 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Formatter/ReflectorFormatter.php: -------------------------------------------------------------------------------- 1 | inputString = $inputString; 33 | } 34 | 35 | /** 36 | * To. String. 37 | */ 38 | public function __toString(): string 39 | { 40 | return $this->inputString; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Output/OutputPager.php: -------------------------------------------------------------------------------- 1 | getStream()); 30 | } 31 | 32 | /** 33 | * Close the current pager process. 34 | */ 35 | public function close() 36 | { 37 | // nothing to do here 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ParserFactory.php: -------------------------------------------------------------------------------- 1 | create(OriginalParserFactory::PREFER_PHP7); 31 | } 32 | 33 | return $factory->createForHostVersion(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Readline/Hoa/Autocompleter.php: -------------------------------------------------------------------------------- 1 | setData($data); 60 | 61 | return; 62 | } 63 | 64 | /** 65 | * Sends this object on the event channel. 66 | */ 67 | public function send(string $eventId, EventSource $source) 68 | { 69 | return Event::notify($eventId, $source, $this); 70 | } 71 | 72 | /** 73 | * Sets a new source. 74 | */ 75 | public function setSource(EventSource $source) 76 | { 77 | $old = $this->_source; 78 | $this->_source = $source; 79 | 80 | return $old; 81 | } 82 | 83 | /** 84 | * Returns the source. 85 | */ 86 | public function getSource() 87 | { 88 | return $this->_source; 89 | } 90 | 91 | /** 92 | * Sets new data. 93 | */ 94 | public function setData($data) 95 | { 96 | $old = $this->_data; 97 | $this->_data = $data; 98 | 99 | return $old; 100 | } 101 | 102 | /** 103 | * Returns the data. 104 | */ 105 | public function getData() 106 | { 107 | return $this->_data; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Readline/Hoa/EventException.php: -------------------------------------------------------------------------------- 1 | getListener(); 55 | 56 | if (null === $listener) { 57 | throw new EventException('Cannot attach a callable to the listener %s because '.'it has not been initialized yet.', 0, static::class); 58 | } 59 | 60 | $listener->attach($listenerId, $callable); 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Sets a new listener. 67 | */ 68 | protected function setListener(EventListener $listener) 69 | { 70 | $old = $this->_listener; 71 | $this->_listener = $listener; 72 | 73 | return $old; 74 | } 75 | 76 | /** 77 | * Returns the listener. 78 | */ 79 | protected function getListener() 80 | { 81 | return $this->_listener; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Readline/Hoa/EventSource.php: -------------------------------------------------------------------------------- 1 | send(); 64 | 65 | return; 66 | } 67 | 68 | /** 69 | * Sends the exception on `hoa://Event/Exception`. 70 | */ 71 | public function send() 72 | { 73 | Event::notify( 74 | 'hoa://Event/Exception', 75 | $this, 76 | new EventBucket($this) 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Readline/Hoa/FileDoesNotExistException.php: -------------------------------------------------------------------------------- 1 | _splFileInfoClass = $splFileInfoClass; 59 | 60 | if (null === $flags) { 61 | parent::__construct($path); 62 | } else { 63 | parent::__construct($path, $flags); 64 | } 65 | 66 | return; 67 | } 68 | 69 | /** 70 | * Current. 71 | * Please, see \FileSystemIterator::current() method. 72 | */ 73 | #[\ReturnTypeWillChange] 74 | public function current() 75 | { 76 | $out = parent::current(); 77 | 78 | if (null !== $this->_splFileInfoClass && 79 | $out instanceof \SplFileInfo) { 80 | $out->setInfoClass($this->_splFileInfoClass); 81 | $out = $out->getFileInfo(); 82 | } 83 | 84 | return $out; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Readline/Hoa/ProtocolException.php: -------------------------------------------------------------------------------- 1 | read(). 58 | */ 59 | public function readString(int $length); 60 | 61 | /** 62 | * Read a character. 63 | * It could be equivalent to $this->read(1). 64 | */ 65 | public function readCharacter(); 66 | 67 | /** 68 | * Read a boolean. 69 | */ 70 | public function readBoolean(); 71 | 72 | /** 73 | * Read an integer. 74 | */ 75 | public function readInteger(int $length = 1); 76 | 77 | /** 78 | * Read a float. 79 | */ 80 | public function readFloat(int $length = 1); 81 | 82 | /** 83 | * Read an array. 84 | * In most cases, it could be an alias to the $this->scanf() method. 85 | */ 86 | public function readArray(); 87 | 88 | /** 89 | * Read a line. 90 | */ 91 | public function readLine(); 92 | 93 | /** 94 | * Read all, i.e. read as much as possible. 95 | */ 96 | public function readAll(int $offset = 0); 97 | 98 | /** 99 | * Parse input from a stream according to a format. 100 | */ 101 | public function scanf(string $format): array; 102 | } 103 | -------------------------------------------------------------------------------- /src/Readline/Hoa/StreamLockable.php: -------------------------------------------------------------------------------- 1 | lock() to block while locking. 71 | * 72 | * @const int 73 | */ 74 | const LOCK_NO_BLOCK = \LOCK_NB; 75 | 76 | /** 77 | * Portable advisory locking. 78 | * Should take a look at stream_supports_lock(). 79 | * 80 | * @param int $operation operation, use the self::LOCK_* constants 81 | * 82 | * @return bool 83 | */ 84 | public function lock(int $operation): bool; 85 | } 86 | -------------------------------------------------------------------------------- /src/Readline/Hoa/StreamOut.php: -------------------------------------------------------------------------------- 1 | function = $function; 35 | $this->parameter = $parameter; 36 | $this->opts = $opts; 37 | } 38 | 39 | /** 40 | * No class here. 41 | */ 42 | public function getClass(): ?\ReflectionClass 43 | { 44 | return null; 45 | } 46 | 47 | /** 48 | * Is the param an array? 49 | * 50 | * @return bool 51 | */ 52 | public function isArray(): bool 53 | { 54 | return \array_key_exists('isArray', $this->opts) && $this->opts['isArray']; 55 | } 56 | 57 | /** 58 | * Get param default value. 59 | * 60 | * @todo remove \ReturnTypeWillChange attribute after dropping support for PHP 7.x (when we can use mixed type) 61 | * 62 | * @return mixed 63 | */ 64 | #[\ReturnTypeWillChange] 65 | public function getDefaultValue() 66 | { 67 | if ($this->isDefaultValueAvailable()) { 68 | return $this->opts['defaultValue']; 69 | } 70 | 71 | return null; 72 | } 73 | 74 | /** 75 | * Get param name. 76 | * 77 | * @return string 78 | */ 79 | public function getName(): string 80 | { 81 | return $this->parameter; 82 | } 83 | 84 | /** 85 | * Is the param optional? 86 | * 87 | * @return bool 88 | */ 89 | public function isOptional(): bool 90 | { 91 | return \array_key_exists('isOptional', $this->opts) && $this->opts['isOptional']; 92 | } 93 | 94 | /** 95 | * Does the param have a default value? 96 | * 97 | * @return bool 98 | */ 99 | public function isDefaultValueAvailable(): bool 100 | { 101 | return \array_key_exists('defaultValue', $this->opts); 102 | } 103 | 104 | /** 105 | * Is the param passed by reference? 106 | * 107 | * (I don't think this is true for anything we need to fake a param for) 108 | * 109 | * @return bool 110 | */ 111 | public function isPassedByReference(): bool 112 | { 113 | return \array_key_exists('isPassedByReference', $this->opts) && $this->opts['isPassedByReference']; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Reflection/ReflectionNamespace.php: -------------------------------------------------------------------------------- 1 | name = $name; 29 | } 30 | 31 | /** 32 | * Gets the constant name. 33 | * 34 | * @return string 35 | */ 36 | public function getName(): string 37 | { 38 | return $this->name; 39 | } 40 | 41 | /** 42 | * This can't (and shouldn't) do anything :). 43 | * 44 | * @throws \RuntimeException 45 | */ 46 | public static function export($name) 47 | { 48 | throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)'); 49 | } 50 | 51 | /** 52 | * To string. 53 | * 54 | * @return string 55 | */ 56 | public function __toString(): string 57 | { 58 | return $this->getName(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/SuperglobalsEnv.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class AutoCompleter 22 | { 23 | /** @var Matcher\AbstractMatcher[] */ 24 | protected $matchers; 25 | 26 | /** 27 | * Register a tab completion Matcher. 28 | * 29 | * @param AbstractMatcher $matcher 30 | */ 31 | public function addMatcher(AbstractMatcher $matcher) 32 | { 33 | $this->matchers[] = $matcher; 34 | } 35 | 36 | /** 37 | * Activate readline tab completion. 38 | */ 39 | public function activate() 40 | { 41 | \readline_completion_function([&$this, 'callback']); 42 | } 43 | 44 | /** 45 | * Handle readline completion. 46 | * 47 | * @param string $input Readline current word 48 | * @param int $index Current word index 49 | * @param array $info readline_info() data 50 | * 51 | * @return array 52 | */ 53 | public function processCallback(string $input, int $index, array $info = []): array 54 | { 55 | // Some (Windows?) systems provide incomplete `readline_info`, so let's 56 | // try to work around it. 57 | $line = $info['line_buffer']; 58 | if (isset($info['end'])) { 59 | $line = \substr($line, 0, $info['end']); 60 | } 61 | if ($line === '' && $input !== '') { 62 | $line = $input; 63 | } 64 | 65 | $tokens = \token_get_all('matchers as $matcher) { 76 | if ($matcher->hasMatched($tokens)) { 77 | $matches = \array_merge($matcher->getMatches($tokens, $info), $matches); 78 | } 79 | } 80 | 81 | $matches = \array_unique($matches); 82 | 83 | return !empty($matches) ? $matches : ['']; 84 | } 85 | 86 | /** 87 | * The readline_completion_function callback handler. 88 | * 89 | * @see processCallback 90 | * 91 | * @param string $input 92 | * @param int $index 93 | * 94 | * @return array 95 | */ 96 | public function callback(string $input, int $index): array 97 | { 98 | return $this->processCallback($input, $index, \readline_info()); 99 | } 100 | 101 | /** 102 | * Remove readline callback handler on destruct. 103 | */ 104 | public function __destruct() 105 | { 106 | // PHP didn't implement the whole readline API when they first switched 107 | // to libedit. And they still haven't. 108 | if (\function_exists('readline_callback_handler_remove')) { 109 | \readline_callback_handler_remove(); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/AbstractContextAwareMatcher.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | abstract class AbstractContextAwareMatcher extends AbstractMatcher implements ContextAware 26 | { 27 | /** 28 | * Context instance (for ContextAware interface). 29 | * 30 | * @var Context 31 | */ 32 | protected $context; 33 | 34 | /** 35 | * ContextAware interface. 36 | * 37 | * @param Context $context 38 | */ 39 | public function setContext(Context $context) 40 | { 41 | $this->context = $context; 42 | } 43 | 44 | /** 45 | * Get a Context variable by name. 46 | * 47 | * @param string $var Variable name 48 | * 49 | * @return mixed 50 | */ 51 | protected function getVariable(string $var) 52 | { 53 | return $this->context->get($var); 54 | } 55 | 56 | /** 57 | * Get all variables in the current Context. 58 | * 59 | * @return array 60 | */ 61 | protected function getVariables(): array 62 | { 63 | return $this->context->getAll(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/AbstractDefaultParametersMatcher.php: -------------------------------------------------------------------------------- 1 | isDefaultValueAvailable()) { 27 | return []; 28 | } 29 | 30 | $defaultValue = $this->valueToShortString($parameter->getDefaultValue()); 31 | 32 | $parametersProcessed[] = \sprintf('$%s = %s', $parameter->getName(), $defaultValue); 33 | } 34 | 35 | if (empty($parametersProcessed)) { 36 | return []; 37 | } 38 | 39 | return [\implode(', ', $parametersProcessed).')']; 40 | } 41 | 42 | /** 43 | * Takes in the default value of a parameter and turns it into a 44 | * string representation that fits inline. 45 | * This is not 100% true to the original (newlines are inlined, for example). 46 | * 47 | * @param mixed $value 48 | */ 49 | private function valueToShortString($value): string 50 | { 51 | if (!\is_array($value)) { 52 | return \json_encode($value); 53 | } 54 | 55 | $chunks = []; 56 | $chunksSequential = []; 57 | 58 | $allSequential = true; 59 | 60 | foreach ($value as $key => $item) { 61 | $allSequential = $allSequential && \is_numeric($key) && $key === \count($chunksSequential); 62 | 63 | $keyString = $this->valueToShortString($key); 64 | $itemString = $this->valueToShortString($item); 65 | 66 | $chunks[] = "{$keyString} => {$itemString}"; 67 | $chunksSequential[] = $itemString; 68 | } 69 | 70 | $chunksToImplode = $allSequential ? $chunksSequential : $chunks; 71 | 72 | return '['.\implode(', ', $chunksToImplode).']'; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/ClassAttributesMatcher.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class ClassAttributesMatcher extends AbstractMatcher 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getMatches(array $tokens, array $info = []): array 28 | { 29 | $input = $this->getInput($tokens); 30 | 31 | $firstToken = \array_pop($tokens); 32 | if (self::tokenIs($firstToken, self::T_STRING)) { 33 | // second token is the nekudotayim operator 34 | \array_pop($tokens); 35 | } 36 | 37 | $class = $this->getNamespaceAndClass($tokens); 38 | 39 | try { 40 | $reflection = new \ReflectionClass($class); 41 | } catch (\ReflectionException $re) { 42 | return []; 43 | } 44 | 45 | $vars = \array_merge( 46 | \array_map( 47 | function ($var) { 48 | return '$'.$var; 49 | }, 50 | \array_keys($reflection->getStaticProperties()) 51 | ), 52 | \array_keys($reflection->getConstants()) 53 | ); 54 | 55 | return \array_map( 56 | function ($name) use ($class) { 57 | $chunks = \explode('\\', $class); 58 | $className = \array_pop($chunks); 59 | 60 | return $className.'::'.$name; 61 | }, 62 | \array_filter( 63 | $vars, 64 | function ($var) use ($input) { 65 | return AbstractMatcher::startsWith($input, $var); 66 | } 67 | ) 68 | ); 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function hasMatched(array $tokens): bool 75 | { 76 | $token = \array_pop($tokens); 77 | $prevToken = \array_pop($tokens); 78 | 79 | switch (true) { 80 | case self::tokenIs($prevToken, self::T_DOUBLE_COLON) && self::tokenIs($token, self::T_STRING): 81 | case self::tokenIs($token, self::T_DOUBLE_COLON): 82 | return true; 83 | } 84 | 85 | return false; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php: -------------------------------------------------------------------------------- 1 | getNamespaceAndClass($tokens); 23 | 24 | try { 25 | $reflection = new \ReflectionClass($class); 26 | } catch (\ReflectionException $e) { 27 | // In this case the class apparently does not exist, so we can do nothing 28 | return []; 29 | } 30 | 31 | $methods = $reflection->getMethods(\ReflectionMethod::IS_STATIC); 32 | 33 | foreach ($methods as $method) { 34 | if ($method->getName() === $functionName[1]) { 35 | return $this->getDefaultParameterCompletion($method->getParameters()); 36 | } 37 | } 38 | 39 | return []; 40 | } 41 | 42 | public function hasMatched(array $tokens): bool 43 | { 44 | $openBracket = \array_pop($tokens); 45 | 46 | if ($openBracket !== '(') { 47 | return false; 48 | } 49 | 50 | $functionName = \array_pop($tokens); 51 | 52 | if (!self::tokenIs($functionName, self::T_STRING)) { 53 | return false; 54 | } 55 | 56 | $operator = \array_pop($tokens); 57 | 58 | if (!self::tokenIs($operator, self::T_DOUBLE_COLON)) { 59 | return false; 60 | } 61 | 62 | return true; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/ClassMethodsMatcher.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class ClassMethodsMatcher extends AbstractMatcher 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getMatches(array $tokens, array $info = []): array 28 | { 29 | $input = $this->getInput($tokens); 30 | 31 | $firstToken = \array_pop($tokens); 32 | if (self::tokenIs($firstToken, self::T_STRING)) { 33 | // second token is the nekudotayim operator 34 | \array_pop($tokens); 35 | } 36 | 37 | $class = $this->getNamespaceAndClass($tokens); 38 | 39 | try { 40 | $reflection = new \ReflectionClass($class); 41 | } catch (\ReflectionException $re) { 42 | return []; 43 | } 44 | 45 | if (self::needCompleteClass($tokens[1])) { 46 | $methods = $reflection->getMethods(); 47 | } else { 48 | $methods = $reflection->getMethods(\ReflectionMethod::IS_STATIC); 49 | } 50 | 51 | $methods = \array_map(function (\ReflectionMethod $method) { 52 | return $method->getName(); 53 | }, $methods); 54 | 55 | return \array_map( 56 | function ($name) use ($class) { 57 | $chunks = \explode('\\', $class); 58 | $className = \array_pop($chunks); 59 | 60 | return $className.'::'.$name; 61 | }, 62 | \array_filter($methods, function ($method) use ($input) { 63 | return AbstractMatcher::startsWith($input, $method); 64 | }) 65 | ); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function hasMatched(array $tokens): bool 72 | { 73 | $token = \array_pop($tokens); 74 | $prevToken = \array_pop($tokens); 75 | 76 | switch (true) { 77 | case self::tokenIs($prevToken, self::T_DOUBLE_COLON) && self::tokenIs($token, self::T_STRING): 78 | case self::tokenIs($token, self::T_DOUBLE_COLON): 79 | return true; 80 | } 81 | 82 | return false; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/ClassNamesMatcher.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ClassNamesMatcher extends AbstractMatcher 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function getMatches(array $tokens, array $info = []): array 27 | { 28 | $class = $this->getNamespaceAndClass($tokens); 29 | if ($class !== '' && $class[0] === '\\') { 30 | $class = \substr($class, 1, \strlen($class)); 31 | } 32 | $quotedClass = \preg_quote($class); 33 | 34 | return \array_map( 35 | function ($className) use ($class) { 36 | // get the number of namespace separators 37 | $nsPos = \substr_count($class, '\\'); 38 | $pieces = \explode('\\', $className); 39 | 40 | // $methods = Mirror::get($class); 41 | return \implode('\\', \array_slice($pieces, $nsPos, \count($pieces))); 42 | }, 43 | \array_filter( 44 | \array_merge(\get_declared_classes(), \get_declared_interfaces()), 45 | function ($className) use ($quotedClass) { 46 | return AbstractMatcher::startsWith($quotedClass, $className); 47 | } 48 | ) 49 | ); 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function hasMatched(array $tokens): bool 56 | { 57 | $token = \array_pop($tokens); 58 | $prevToken = \array_pop($tokens); 59 | 60 | $ignoredTokens = [ 61 | self::T_INCLUDE, self::T_INCLUDE_ONCE, self::T_REQUIRE, self::T_REQUIRE_ONCE, 62 | ]; 63 | 64 | switch (true) { 65 | case self::hasToken([$ignoredTokens], $token): 66 | case self::hasToken([$ignoredTokens], $prevToken): 67 | case \is_string($token) && $token === '$': 68 | return false; 69 | case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR, self::T_STRING], $prevToken): 70 | case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR], $token): 71 | case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): 72 | case self::isOperator($token): 73 | return true; 74 | } 75 | 76 | return false; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/CommandsMatcher.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class CommandsMatcher extends AbstractMatcher 25 | { 26 | /** @var string[] */ 27 | protected $commands = []; 28 | 29 | /** 30 | * CommandsMatcher constructor. 31 | * 32 | * @param Command[] $commands 33 | */ 34 | public function __construct(array $commands) 35 | { 36 | $this->setCommands($commands); 37 | } 38 | 39 | /** 40 | * Set Commands for completion. 41 | * 42 | * @param Command[] $commands 43 | */ 44 | public function setCommands(array $commands) 45 | { 46 | $names = []; 47 | foreach ($commands as $command) { 48 | $names = \array_merge([$command->getName()], $names); 49 | $names = \array_merge($command->getAliases(), $names); 50 | } 51 | $this->commands = $names; 52 | } 53 | 54 | /** 55 | * Check whether a command $name is defined. 56 | * 57 | * @param string $name 58 | */ 59 | protected function isCommand(string $name): bool 60 | { 61 | return \in_array($name, $this->commands); 62 | } 63 | 64 | /** 65 | * Check whether input matches a defined command. 66 | * 67 | * @param string $name 68 | */ 69 | protected function matchCommand(string $name): bool 70 | { 71 | foreach ($this->commands as $cmd) { 72 | if ($this->startsWith($name, $cmd)) { 73 | return true; 74 | } 75 | } 76 | 77 | return false; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function getMatches(array $tokens, array $info = []): array 84 | { 85 | $input = $this->getInput($tokens); 86 | 87 | return \array_filter($this->commands, function ($command) use ($input) { 88 | return AbstractMatcher::startsWith($input, $command); 89 | }); 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function hasMatched(array $tokens): bool 96 | { 97 | /* $openTag */ \array_shift($tokens); 98 | $command = \array_shift($tokens); 99 | 100 | switch (true) { 101 | case self::tokenIs($command, self::T_STRING) && 102 | !$this->isCommand($command[1]) && 103 | $this->matchCommand($command[1]) && 104 | empty($tokens): 105 | return true; 106 | } 107 | 108 | return false; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/ConstantsMatcher.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ConstantsMatcher extends AbstractMatcher 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function getMatches(array $tokens, array $info = []): array 27 | { 28 | $const = $this->getInput($tokens); 29 | 30 | return \array_filter(\array_keys(\get_defined_constants()), function ($constant) use ($const) { 31 | return AbstractMatcher::startsWith($const, $constant); 32 | }); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function hasMatched(array $tokens): bool 39 | { 40 | $token = \array_pop($tokens); 41 | $prevToken = \array_pop($tokens); 42 | 43 | switch (true) { 44 | case self::tokenIs($prevToken, self::T_NEW): 45 | case self::tokenIs($prevToken, self::T_NS_SEPARATOR): 46 | return false; 47 | case self::hasToken([self::T_OPEN_TAG, self::T_STRING], $token): 48 | case self::isOperator($token): 49 | return true; 50 | } 51 | 52 | return false; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php: -------------------------------------------------------------------------------- 1 | getParameters(); 29 | 30 | return $this->getDefaultParameterCompletion($parameters); 31 | } 32 | 33 | public function hasMatched(array $tokens): bool 34 | { 35 | $openBracket = \array_pop($tokens); 36 | 37 | if ($openBracket !== '(') { 38 | return false; 39 | } 40 | 41 | $functionName = \array_pop($tokens); 42 | 43 | if (!self::tokenIs($functionName, self::T_STRING)) { 44 | return false; 45 | } 46 | 47 | if (!\function_exists($functionName[1])) { 48 | return false; 49 | } 50 | 51 | return true; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/FunctionsMatcher.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class FunctionsMatcher extends AbstractMatcher 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function getMatches(array $tokens, array $info = []): array 27 | { 28 | $func = $this->getInput($tokens); 29 | 30 | $functions = \get_defined_functions(); 31 | $allFunctions = \array_merge($functions['user'], $functions['internal']); 32 | 33 | return \array_filter($allFunctions, function ($function) use ($func) { 34 | return AbstractMatcher::startsWith($func, $function); 35 | }); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function hasMatched(array $tokens): bool 42 | { 43 | $token = \array_pop($tokens); 44 | $prevToken = \array_pop($tokens); 45 | 46 | switch (true) { 47 | case self::tokenIs($prevToken, self::T_NEW): 48 | return false; 49 | case self::hasToken([self::T_OPEN_TAG, self::T_STRING], $token): 50 | case self::isOperator($token): 51 | return true; 52 | } 53 | 54 | return false; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/KeywordsMatcher.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class KeywordsMatcher extends AbstractMatcher 22 | { 23 | protected $keywords = [ 24 | 'array', 'clone', 'declare', 'die', 'echo', 'empty', 'eval', 'exit', 'include', 25 | 'include_once', 'isset', 'list', 'print', 'require', 'require_once', 'unset', 26 | ]; 27 | 28 | protected $mandatoryStartKeywords = [ 29 | 'die', 'echo', 'print', 'unset', 30 | ]; 31 | 32 | /** 33 | * Get all (completable) PHP keywords. 34 | * 35 | * @return string[] 36 | */ 37 | public function getKeywords(): array 38 | { 39 | return $this->keywords; 40 | } 41 | 42 | /** 43 | * Check whether $keyword is a (completable) PHP keyword. 44 | * 45 | * @param string $keyword 46 | */ 47 | public function isKeyword(string $keyword): bool 48 | { 49 | return \in_array($keyword, $this->keywords); 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function getMatches(array $tokens, array $info = []): array 56 | { 57 | $input = $this->getInput($tokens); 58 | 59 | return \array_filter($this->keywords, function ($keyword) use ($input) { 60 | return AbstractMatcher::startsWith($input, $keyword); 61 | }); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function hasMatched(array $tokens): bool 68 | { 69 | $token = \array_pop($tokens); 70 | $prevToken = \array_pop($tokens); 71 | 72 | switch (true) { 73 | case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): 74 | // case is_string($token) && $token === '$': 75 | case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $prevToken) && 76 | self::tokenIs($token, self::T_STRING): 77 | case self::isOperator($token): 78 | return true; 79 | } 80 | 81 | return false; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/MongoClientMatcher.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class MongoClientMatcher extends AbstractContextAwareMatcher 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function getMatches(array $tokens, array $info = []): array 27 | { 28 | $input = $this->getInput($tokens); 29 | 30 | $firstToken = \array_pop($tokens); 31 | if (self::tokenIs($firstToken, self::T_STRING)) { 32 | // second token is the object operator 33 | \array_pop($tokens); 34 | } 35 | $objectToken = \array_pop($tokens); 36 | $objectName = \str_replace('$', '', $objectToken[1]); 37 | $object = $this->getVariable($objectName); 38 | 39 | if (!$object instanceof \MongoClient) { 40 | return []; 41 | } 42 | 43 | $list = $object->listDBs(); 44 | 45 | return \array_filter( 46 | \array_map(function ($info) { 47 | return $info['name']; 48 | }, $list['databases']), 49 | function ($var) use ($input) { 50 | return AbstractMatcher::startsWith($input, $var); 51 | } 52 | ); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function hasMatched(array $tokens): bool 59 | { 60 | $token = \array_pop($tokens); 61 | $prevToken = \array_pop($tokens); 62 | 63 | switch (true) { 64 | case self::tokenIs($token, self::T_OBJECT_OPERATOR): 65 | case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): 66 | return true; 67 | } 68 | 69 | return false; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/MongoDatabaseMatcher.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class MongoDatabaseMatcher extends AbstractContextAwareMatcher 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function getMatches(array $tokens, array $info = []): array 27 | { 28 | $input = $this->getInput($tokens); 29 | 30 | $firstToken = \array_pop($tokens); 31 | if (self::tokenIs($firstToken, self::T_STRING)) { 32 | // second token is the object operator 33 | \array_pop($tokens); 34 | } 35 | $objectToken = \array_pop($tokens); 36 | $objectName = \str_replace('$', '', $objectToken[1]); 37 | $object = $this->getVariable($objectName); 38 | 39 | if (!$object instanceof \MongoDB) { 40 | return []; 41 | } 42 | 43 | return \array_filter( 44 | $object->getCollectionNames(), 45 | function ($var) use ($input) { 46 | return AbstractMatcher::startsWith($input, $var); 47 | } 48 | ); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function hasMatched(array $tokens): bool 55 | { 56 | $token = \array_pop($tokens); 57 | $prevToken = \array_pop($tokens); 58 | 59 | switch (true) { 60 | case self::tokenIs($token, self::T_OBJECT_OPERATOR): 61 | case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): 62 | return true; 63 | } 64 | 65 | return false; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/ObjectAttributesMatcher.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class ObjectAttributesMatcher extends AbstractContextAwareMatcher 25 | { 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getMatches(array $tokens, array $info = []): array 30 | { 31 | $input = $this->getInput($tokens); 32 | 33 | $firstToken = \array_pop($tokens); 34 | if (self::tokenIs($firstToken, self::T_STRING)) { 35 | // second token is the object operator 36 | \array_pop($tokens); 37 | } 38 | $objectToken = \array_pop($tokens); 39 | if (!\is_array($objectToken)) { 40 | return []; 41 | } 42 | $objectName = \str_replace('$', '', $objectToken[1]); 43 | 44 | try { 45 | $object = $this->getVariable($objectName); 46 | } catch (InvalidArgumentException $e) { 47 | return []; 48 | } 49 | 50 | if (!\is_object($object)) { 51 | return []; 52 | } 53 | 54 | return \array_filter( 55 | \array_keys(\get_class_vars(\get_class($object))), 56 | function ($var) use ($input) { 57 | return AbstractMatcher::startsWith($input, $var); 58 | } 59 | ); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function hasMatched(array $tokens): bool 66 | { 67 | $token = \array_pop($tokens); 68 | $prevToken = \array_pop($tokens); 69 | 70 | switch (true) { 71 | case self::tokenIs($token, self::T_OBJECT_OPERATOR): 72 | case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): 73 | return true; 74 | } 75 | 76 | return false; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php: -------------------------------------------------------------------------------- 1 | getVariable($objectName); 31 | $reflection = new \ReflectionObject($object); 32 | } catch (\InvalidArgumentException $e) { 33 | return []; 34 | } catch (\ReflectionException $e) { 35 | return []; 36 | } 37 | 38 | $methods = $reflection->getMethods(); 39 | 40 | foreach ($methods as $method) { 41 | if ($method->getName() === $functionName[1]) { 42 | return $this->getDefaultParameterCompletion($method->getParameters()); 43 | } 44 | } 45 | 46 | return []; 47 | } 48 | 49 | public function hasMatched(array $tokens): bool 50 | { 51 | $openBracket = \array_pop($tokens); 52 | 53 | if ($openBracket !== '(') { 54 | return false; 55 | } 56 | 57 | $functionName = \array_pop($tokens); 58 | 59 | if (!self::tokenIs($functionName, self::T_STRING)) { 60 | return false; 61 | } 62 | 63 | $operator = \array_pop($tokens); 64 | 65 | if (!self::tokenIs($operator, self::T_OBJECT_OPERATOR)) { 66 | return false; 67 | } 68 | 69 | return true; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/ObjectMethodsMatcher.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class ObjectMethodsMatcher extends AbstractContextAwareMatcher 25 | { 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getMatches(array $tokens, array $info = []): array 30 | { 31 | $input = $this->getInput($tokens); 32 | 33 | $firstToken = \array_pop($tokens); 34 | if (self::tokenIs($firstToken, self::T_STRING)) { 35 | // second token is the object operator 36 | \array_pop($tokens); 37 | } 38 | $objectToken = \array_pop($tokens); 39 | if (!\is_array($objectToken)) { 40 | return []; 41 | } 42 | $objectName = \str_replace('$', '', $objectToken[1]); 43 | 44 | try { 45 | $object = $this->getVariable($objectName); 46 | } catch (InvalidArgumentException $e) { 47 | return []; 48 | } 49 | 50 | if (!\is_object($object)) { 51 | return []; 52 | } 53 | 54 | return \array_filter( 55 | \get_class_methods($object), 56 | function ($var) use ($input) { 57 | return AbstractMatcher::startsWith($input, $var) && 58 | // also check that we do not suggest invoking a super method(__construct, __wakeup, …) 59 | !AbstractMatcher::startsWith('__', $var); 60 | } 61 | ); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function hasMatched(array $tokens): bool 68 | { 69 | $token = \array_pop($tokens); 70 | $prevToken = \array_pop($tokens); 71 | 72 | switch (true) { 73 | case self::tokenIs($token, self::T_OBJECT_OPERATOR): 74 | case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): 75 | return true; 76 | } 77 | 78 | return false; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/TabCompletion/Matcher/VariablesMatcher.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class VariablesMatcher extends AbstractContextAwareMatcher 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function getMatches(array $tokens, array $info = []): array 27 | { 28 | $var = \str_replace('$', '', $this->getInput($tokens)); 29 | 30 | return \array_filter(\array_keys($this->getVariables()), function ($variable) use ($var) { 31 | return AbstractMatcher::startsWith($var, $variable); 32 | }); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function hasMatched(array $tokens): bool 39 | { 40 | $token = \array_pop($tokens); 41 | 42 | switch (true) { 43 | case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): 44 | case \is_string($token) && $token === '$': 45 | case self::isOperator($token): 46 | return true; 47 | } 48 | 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Util/Json.php: -------------------------------------------------------------------------------- 1 | filter = $filter; 32 | 33 | return parent::cloneVar($var, $filter); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | protected function castResource(Stub $stub, $isNested): array 40 | { 41 | return Caster::EXCLUDE_VERBOSE & $this->filter ? [] : parent::castResource($stub, $isNested); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/VarDumper/Dumper.php: -------------------------------------------------------------------------------- 1 | '\0', 30 | "\t" => '\t', 31 | "\n" => '\n', 32 | "\v" => '\v', 33 | "\f" => '\f', 34 | "\r" => '\r', 35 | "\033" => '\e', 36 | ]; 37 | 38 | public function __construct(OutputFormatter $formatter, $forceArrayIndexes = false) 39 | { 40 | $this->formatter = $formatter; 41 | $this->forceArrayIndexes = $forceArrayIndexes; 42 | parent::__construct(); 43 | $this->setColors(false); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function enterHash(Cursor $cursor, $type, $class, $hasChild): void 50 | { 51 | if (Cursor::HASH_INDEXED === $type || Cursor::HASH_ASSOC === $type) { 52 | $class = 0; 53 | } 54 | parent::enterHash($cursor, $type, $class, $hasChild); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | protected function dumpKey(Cursor $cursor): void 61 | { 62 | if ($this->forceArrayIndexes || Cursor::HASH_INDEXED !== $cursor->hashType) { 63 | parent::dumpKey($cursor); 64 | } 65 | } 66 | 67 | protected function style($style, $value, $attr = []): string 68 | { 69 | if ('ref' === $style) { 70 | $value = \strtr($value, '@', '#'); 71 | } 72 | 73 | $styled = ''; 74 | $cchr = $this->styles['cchr']; 75 | 76 | $chunks = \preg_split(self::CONTROL_CHARS, $value, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); 77 | foreach ($chunks as $chunk) { 78 | if (\preg_match(self::ONLY_CONTROL_CHARS, $chunk)) { 79 | $chars = ''; 80 | $i = 0; 81 | do { 82 | $chars .= isset(self::CONTROL_CHARS_MAP[$chunk[$i]]) ? self::CONTROL_CHARS_MAP[$chunk[$i]] : \sprintf('\x%02X', \ord($chunk[$i])); 83 | } while (isset($chunk[++$i])); 84 | 85 | $chars = $this->formatter->escape($chars); 86 | $styled .= "<{$cchr}>{$chars}"; 87 | } else { 88 | $styled .= $this->formatter->escape($chunk); 89 | } 90 | } 91 | 92 | $style = $this->styles[$style]; 93 | 94 | return "<{$style}>{$styled}"; 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | protected function dumpLine($depth, $endOfValue = false): void 101 | { 102 | if ($endOfValue && 0 < $depth) { 103 | $this->line .= ','; 104 | } 105 | $this->line = $this->formatter->format($this->line); 106 | parent::dumpLine($depth, $endOfValue); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/VarDumper/PresenterAware.php: -------------------------------------------------------------------------------- 1 | tempDir = $tempDir; 28 | } 29 | 30 | /** {@inheritDoc} */ 31 | public function download(string $url): bool 32 | { 33 | $tempDir = $this->tempDir ?: \sys_get_temp_dir(); 34 | $this->outputFile = \tempnam($tempDir, 'psysh-archive-'); 35 | $targetName = $this->outputFile.'.tar.gz'; 36 | 37 | if (!\rename($this->outputFile, $targetName)) { 38 | return false; 39 | } 40 | 41 | $this->outputFile = $targetName; 42 | 43 | $outputHandle = \fopen($this->outputFile, 'w'); 44 | if (!$outputHandle) { 45 | return false; 46 | } 47 | $curl = \curl_init(); 48 | \curl_setopt_array($curl, [ 49 | \CURLOPT_FAILONERROR => true, 50 | \CURLOPT_HEADER => 0, 51 | \CURLOPT_FOLLOWLOCATION => true, 52 | \CURLOPT_TIMEOUT => 10, 53 | \CURLOPT_FILE => $outputHandle, 54 | \CURLOPT_HTTPHEADER => [ 55 | 'User-Agent' => 'PsySH/'.Shell::VERSION, 56 | ], 57 | ]); 58 | \curl_setopt($curl, \CURLOPT_URL, $url); 59 | $result = \curl_exec($curl); 60 | $error = \curl_error($curl); 61 | \curl_close($curl); 62 | 63 | \fclose($outputHandle); 64 | 65 | if (!$result) { 66 | throw new ErrorException('cURL Error: '.$error); 67 | } 68 | 69 | return (bool) $result; 70 | } 71 | 72 | /** {@inheritDoc} */ 73 | public function getFilename(): string 74 | { 75 | if ($this->outputFile === null) { 76 | throw new RuntimeException('Call download() first'); 77 | } 78 | 79 | return $this->outputFile; 80 | } 81 | 82 | /** {@inheritDoc} */ 83 | public function cleanup() 84 | { 85 | if ($this->outputFile !== null && \file_exists($this->outputFile)) { 86 | \unlink($this->outputFile); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/VersionUpdater/Downloader/Factory.php: -------------------------------------------------------------------------------- 1 | tempDir = $tempDir; 26 | } 27 | 28 | /** {@inheritDoc} */ 29 | public function download(string $url): bool 30 | { 31 | $tempDir = $this->tempDir ?: \sys_get_temp_dir(); 32 | $this->outputFile = \tempnam($tempDir, 'psysh-archive-'); 33 | $targetName = $this->outputFile.'.tar.gz'; 34 | 35 | if (!\rename($this->outputFile, $targetName)) { 36 | return false; 37 | } 38 | 39 | $this->outputFile = $targetName; 40 | 41 | return (bool) \file_put_contents($this->outputFile, \file_get_contents($url)); 42 | } 43 | 44 | /** {@inheritDoc} */ 45 | public function getFilename(): string 46 | { 47 | if ($this->outputFile === null) { 48 | throw new RuntimeException('Call download() first'); 49 | } 50 | 51 | return $this->outputFile; 52 | } 53 | 54 | /** {@inheritDoc} */ 55 | public function cleanup() 56 | { 57 | if ($this->outputFile !== null && \file_exists($this->outputFile)) { 58 | \unlink($this->outputFile); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/VersionUpdater/GitHubChecker.php: -------------------------------------------------------------------------------- 1 | getLatest(), '>='); 29 | } 30 | 31 | public function getLatest(): string 32 | { 33 | if (!isset($this->latest)) { 34 | $this->setLatest($this->getVersionFromTag()); 35 | } 36 | 37 | return $this->latest; 38 | } 39 | 40 | public function setLatest(string $version) 41 | { 42 | $this->latest = $version; 43 | } 44 | 45 | private function getVersionFromTag(): ?string 46 | { 47 | $contents = $this->fetchLatestRelease(); 48 | if (!$contents || !isset($contents->tag_name)) { 49 | throw new \InvalidArgumentException('Unable to check for updates'); 50 | } 51 | $this->setLatest($contents->tag_name); 52 | 53 | return $this->getLatest(); 54 | } 55 | 56 | /** 57 | * Set to public to make testing easier. 58 | * 59 | * @return mixed 60 | */ 61 | public function fetchLatestRelease() 62 | { 63 | $context = \stream_context_create([ 64 | 'http' => [ 65 | 'user_agent' => 'PsySH/'.Shell::VERSION, 66 | 'timeout' => 1.0, 67 | ], 68 | ]); 69 | 70 | \set_error_handler(function () { 71 | // Just ignore all errors with this. The checker will throw an exception 72 | // if it doesn't work :) 73 | }); 74 | 75 | $result = @\file_get_contents(self::URL, false, $context); 76 | 77 | \restore_error_handler(); 78 | 79 | return \json_decode($result); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/VersionUpdater/IntervalChecker.php: -------------------------------------------------------------------------------- 1 | cacheFile = $cacheFile; 22 | $this->interval = $interval; 23 | } 24 | 25 | public function fetchLatestRelease() 26 | { 27 | // Read the cached file 28 | $cached = \json_decode(@\file_get_contents($this->cacheFile, false)); 29 | if ($cached && isset($cached->last_check) && isset($cached->release)) { 30 | $now = new \DateTime(); 31 | $lastCheck = new \DateTime($cached->last_check); 32 | if ($lastCheck >= $now->sub($this->getDateInterval())) { 33 | return $cached->release; 34 | } 35 | } 36 | 37 | // Fall back to fetching from GitHub 38 | $release = parent::fetchLatestRelease(); 39 | if ($release && isset($release->tag_name)) { 40 | $this->updateCache($release); 41 | } 42 | 43 | return $release; 44 | } 45 | 46 | /** 47 | * @throws \RuntimeException if interval passed to constructor is not supported 48 | */ 49 | private function getDateInterval(): \DateInterval 50 | { 51 | switch ($this->interval) { 52 | case Checker::DAILY: 53 | return new \DateInterval('P1D'); 54 | case Checker::WEEKLY: 55 | return new \DateInterval('P1W'); 56 | case Checker::MONTHLY: 57 | return new \DateInterval('P1M'); 58 | } 59 | 60 | throw new \RuntimeException('Invalid interval configured'); 61 | } 62 | 63 | private function updateCache($release) 64 | { 65 | $data = [ 66 | 'last_check' => \date(\DATE_ATOM), 67 | 'release' => $release, 68 | ]; 69 | 70 | \file_put_contents($this->cacheFile, \json_encode($data)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/VersionUpdater/NoopChecker.php: -------------------------------------------------------------------------------- 1 |