| 148 |
lars |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
/*
|
|
|
4 |
* This file is part of Psy Shell.
|
|
|
5 |
*
|
|
|
6 |
* (c) 2012-2022 Justin Hileman
|
|
|
7 |
*
|
|
|
8 |
* For the full copyright and license information, please view the LICENSE
|
|
|
9 |
* file that was distributed with this source code.
|
|
|
10 |
*/
|
|
|
11 |
|
|
|
12 |
namespace Psy\Command\TimeitCommand;
|
|
|
13 |
|
|
|
14 |
use PhpParser\Node;
|
|
|
15 |
use PhpParser\Node\Arg;
|
|
|
16 |
use PhpParser\Node\Expr;
|
|
|
17 |
use PhpParser\Node\Expr\StaticCall;
|
|
|
18 |
use PhpParser\Node\FunctionLike;
|
|
|
19 |
use PhpParser\Node\Name\FullyQualified as FullyQualifiedName;
|
|
|
20 |
use PhpParser\Node\Stmt\Expression;
|
|
|
21 |
use PhpParser\Node\Stmt\Return_;
|
|
|
22 |
use PhpParser\NodeVisitorAbstract;
|
|
|
23 |
use Psy\CodeCleaner\NoReturnValue;
|
|
|
24 |
use Psy\Command\TimeitCommand;
|
|
|
25 |
|
|
|
26 |
/**
|
|
|
27 |
* A node visitor for instrumenting code to be executed by the `timeit` command.
|
|
|
28 |
*
|
|
|
29 |
* Injects `TimeitCommand::markStart()` at the start of code to be executed, and
|
|
|
30 |
* `TimeitCommand::markEnd()` at the end, and on top-level return statements.
|
|
|
31 |
*/
|
|
|
32 |
class TimeitVisitor extends NodeVisitorAbstract
|
|
|
33 |
{
|
|
|
34 |
private $functionDepth;
|
|
|
35 |
|
|
|
36 |
/**
|
|
|
37 |
* {@inheritdoc}
|
|
|
38 |
*
|
|
|
39 |
* @return Node[]|null Array of nodes
|
|
|
40 |
*/
|
|
|
41 |
public function beforeTraverse(array $nodes)
|
|
|
42 |
{
|
|
|
43 |
$this->functionDepth = 0;
|
|
|
44 |
}
|
|
|
45 |
|
|
|
46 |
/**
|
|
|
47 |
* {@inheritdoc}
|
|
|
48 |
*
|
|
|
49 |
* @return int|Node|null Replacement node (or special return value)
|
|
|
50 |
*/
|
|
|
51 |
public function enterNode(Node $node)
|
|
|
52 |
{
|
|
|
53 |
// keep track of nested function-like nodes, because they can have
|
|
|
54 |
// returns statements... and we don't want to call markEnd for those.
|
|
|
55 |
if ($node instanceof FunctionLike) {
|
|
|
56 |
$this->functionDepth++;
|
|
|
57 |
|
|
|
58 |
return;
|
|
|
59 |
}
|
|
|
60 |
|
|
|
61 |
// replace any top-level `return` statements with a `markEnd` call
|
|
|
62 |
if ($this->functionDepth === 0 && $node instanceof Return_) {
|
|
|
63 |
return new Return_($this->getEndCall($node->expr), $node->getAttributes());
|
|
|
64 |
}
|
|
|
65 |
}
|
|
|
66 |
|
|
|
67 |
/**
|
|
|
68 |
* {@inheritdoc}
|
|
|
69 |
*
|
|
|
70 |
* @return int|Node|Node[]|null Replacement node (or special return value)
|
|
|
71 |
*/
|
|
|
72 |
public function leaveNode(Node $node)
|
|
|
73 |
{
|
|
|
74 |
if ($node instanceof FunctionLike) {
|
|
|
75 |
$this->functionDepth--;
|
|
|
76 |
}
|
|
|
77 |
}
|
|
|
78 |
|
|
|
79 |
/**
|
|
|
80 |
* {@inheritdoc}
|
|
|
81 |
*
|
|
|
82 |
* @return Node[]|null Array of nodes
|
|
|
83 |
*/
|
|
|
84 |
public function afterTraverse(array $nodes)
|
|
|
85 |
{
|
|
|
86 |
// prepend a `markStart` call
|
|
|
87 |
\array_unshift($nodes, $this->maybeExpression($this->getStartCall()));
|
|
|
88 |
|
|
|
89 |
// append a `markEnd` call (wrapping the final node, if it's an expression)
|
|
|
90 |
$last = $nodes[\count($nodes) - 1];
|
|
|
91 |
if ($last instanceof Expr) {
|
|
|
92 |
\array_pop($nodes);
|
|
|
93 |
$nodes[] = $this->getEndCall($last);
|
|
|
94 |
} elseif ($last instanceof Expression) {
|
|
|
95 |
\array_pop($nodes);
|
|
|
96 |
$nodes[] = new Expression($this->getEndCall($last->expr), $last->getAttributes());
|
|
|
97 |
} elseif ($last instanceof Return_) {
|
|
|
98 |
// nothing to do here, we're already ending with a return call
|
|
|
99 |
} else {
|
|
|
100 |
$nodes[] = $this->maybeExpression($this->getEndCall());
|
|
|
101 |
}
|
|
|
102 |
|
|
|
103 |
return $nodes;
|
|
|
104 |
}
|
|
|
105 |
|
|
|
106 |
/**
|
|
|
107 |
* Get PhpParser AST nodes for a `markStart` call.
|
|
|
108 |
*
|
|
|
109 |
* @return \PhpParser\Node\Expr\StaticCall
|
|
|
110 |
*/
|
|
|
111 |
private function getStartCall(): StaticCall
|
|
|
112 |
{
|
|
|
113 |
return new StaticCall(new FullyQualifiedName(TimeitCommand::class), 'markStart');
|
|
|
114 |
}
|
|
|
115 |
|
|
|
116 |
/**
|
|
|
117 |
* Get PhpParser AST nodes for a `markEnd` call.
|
|
|
118 |
*
|
|
|
119 |
* Optionally pass in a return value.
|
|
|
120 |
*
|
|
|
121 |
* @param Expr|null $arg
|
|
|
122 |
*
|
|
|
123 |
* @return \PhpParser\Node\Expr\StaticCall
|
|
|
124 |
*/
|
|
|
125 |
private function getEndCall(Expr $arg = null): StaticCall
|
|
|
126 |
{
|
|
|
127 |
if ($arg === null) {
|
|
|
128 |
$arg = NoReturnValue::create();
|
|
|
129 |
}
|
|
|
130 |
|
|
|
131 |
return new StaticCall(new FullyQualifiedName(TimeitCommand::class), 'markEnd', [new Arg($arg)]);
|
|
|
132 |
}
|
|
|
133 |
|
|
|
134 |
/**
|
|
|
135 |
* Compatibility shim for PHP Parser 3.x.
|
|
|
136 |
*
|
|
|
137 |
* Wrap $expr in a PhpParser\Node\Stmt\Expression if the class exists.
|
|
|
138 |
*
|
|
|
139 |
* @param \PhpParser\Node $expr
|
|
|
140 |
* @param array $attrs
|
|
|
141 |
*
|
|
|
142 |
* @return \PhpParser\Node\Expr|\PhpParser\Node\Stmt\Expression
|
|
|
143 |
*/
|
|
|
144 |
private function maybeExpression(Node $expr, array $attrs = [])
|
|
|
145 |
{
|
|
|
146 |
return \class_exists(Expression::class) ? new Expression($expr, $attrs) : $expr;
|
|
|
147 |
}
|
|
|
148 |
}
|