├── 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 | [](https://packagist.org/packages/psy/psysh)
7 | [](https://packagist.org/packages/psy/psysh)
8 | [](http://psysh.org)
9 |
10 | [](https://github.com/bobthecow/psysh/actions?query=branch:main)
11 | [](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%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}{$cchr}>";
87 | } else {
88 | $styled .= $this->formatter->escape($chunk);
89 | }
90 | }
91 |
92 | $style = $this->styles[$style];
93 |
94 | return "<{$style}>{$styled}{$style}>";
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 |