Skip to content

Commit 936e4dd

Browse files
authored
Merge pull request #245 from phpDocumentor/feature/182_define_expressions
Add expression evaluation for define names.
2 parents 4f5a896 + c409391 commit 936e4dd

File tree

6 files changed

+144
-18
lines changed

6 files changed

+144
-18
lines changed

src/phpDocumentor/Reflection/Php/Factory/ConstructorPromotion.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
2121
use Webmozart\Assert\Assert;
2222

23-
class ConstructorPromotion extends AbstractFactory
23+
final class ConstructorPromotion extends AbstractFactory
2424
{
2525
/** @var PrettyPrinter */
2626
private $valueConverter;

src/phpDocumentor/Reflection/Php/Factory/Define.php

+37-17
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
use phpDocumentor\Reflection\Php\Constant as ConstantElement;
2020
use phpDocumentor\Reflection\Php\File as FileElement;
2121
use phpDocumentor\Reflection\Php\StrategyContainer;
22+
use phpDocumentor\Reflection\Php\ValueEvaluator\ConstantEvaluator;
23+
use PhpParser\ConstExprEvaluationException;
2224
use PhpParser\Node\Arg;
25+
use PhpParser\Node\Expr;
2326
use PhpParser\Node\Expr\FuncCall;
2427
use PhpParser\Node\Name;
25-
use PhpParser\Node\Scalar\String_;
2628
use PhpParser\Node\Stmt\Expression;
2729
use PhpParser\Node\VariadicPlaceholder;
2830
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
29-
use RuntimeException;
3031

3132
use function assert;
3233
use function sprintf;
@@ -43,13 +44,20 @@ final class Define extends AbstractFactory
4344
/** @var PrettyPrinter */
4445
private $valueConverter;
4546

47+
/** @var ConstantEvaluator */
48+
private $constantEvaluator;
49+
4650
/**
4751
* Initializes the object.
4852
*/
49-
public function __construct(DocBlockFactoryInterface $docBlockFactory, PrettyPrinter $prettyPrinter)
50-
{
53+
public function __construct(
54+
DocBlockFactoryInterface $docBlockFactory,
55+
PrettyPrinter $prettyPrinter,
56+
?ConstantEvaluator $constantEvaluator = null
57+
) {
5158
parent::__construct($docBlockFactory);
5259
$this->valueConverter = $prettyPrinter;
60+
$this->constantEvaluator = $constantEvaluator ?? new ConstantEvaluator();
5361
}
5462

5563
public function matches(ContextStack $context, object $object): bool
@@ -85,12 +93,7 @@ protected function doCreate(
8593
StrategyContainer $strategies
8694
): void {
8795
$expression = $object->expr;
88-
if (!$expression instanceof FuncCall) {
89-
throw new RuntimeException(
90-
'Provided expression is not a function call; this should not happen because the `create` method'
91-
. ' checks the given object again using `matches`'
92-
);
93-
}
96+
assert($expression instanceof FuncCall);
9497

9598
[$name, $value] = $expression->args;
9699

@@ -102,8 +105,13 @@ protected function doCreate(
102105
$file = $context->search(FileElement::class);
103106
assert($file instanceof FileElement);
104107

108+
$fqsen = $this->determineFqsen($name, $context);
109+
if ($fqsen === null) {
110+
return;
111+
}
112+
105113
$constant = new ConstantElement(
106-
$this->determineFqsen($name),
114+
$fqsen,
107115
$this->createDocBlock($object->getDocComment(), $context->getTypeContext()),
108116
$this->determineValue($value),
109117
new Location($object->getLine()),
@@ -122,15 +130,27 @@ private function determineValue(?Arg $value): ?string
122130
return $this->valueConverter->prettyPrintExpr($value->value);
123131
}
124132

125-
private function determineFqsen(Arg $name): Fqsen
133+
private function determineFqsen(Arg $name, ContextStack $context): ?Fqsen
126134
{
127-
$nameString = $name->value;
128-
assert($nameString instanceof String_);
135+
return $this->fqsenFromExpression($name->value, $context);
136+
}
129137

130-
if (strpos($nameString->value, '\\') === false) {
131-
return new Fqsen(sprintf('\\%s', $nameString->value));
138+
private function fqsenFromExpression(Expr $nameString, ContextStack $context): ?Fqsen
139+
{
140+
try {
141+
return $this->fqsenFromString($this->constantEvaluator->evaluate($nameString, $context));
142+
} catch (ConstExprEvaluationException $e) {
143+
//Ignore any errors as we cannot evaluate all expressions
144+
return null;
145+
}
146+
}
147+
148+
private function fqsenFromString(string $nameString): Fqsen
149+
{
150+
if (strpos($nameString, '\\') === false) {
151+
return new Fqsen(sprintf('\\%s', $nameString));
132152
}
133153

134-
return new Fqsen(sprintf('%s', $nameString->value));
154+
return new Fqsen(sprintf('%s', $nameString));
135155
}
136156
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpDocumentor\Reflection\Php\ValueEvaluator;
6+
7+
use phpDocumentor\Reflection\Php\Factory\ContextStack;
8+
use PhpParser\ConstExprEvaluationException;
9+
use PhpParser\ConstExprEvaluator;
10+
use PhpParser\Node;
11+
use PhpParser\Node\Expr;
12+
13+
use function sprintf;
14+
15+
/**
16+
* @internal
17+
*/
18+
final class ConstantEvaluator
19+
{
20+
/** @throws ConstExprEvaluationException */
21+
public function evaluate(Expr $expr, ContextStack $contextStack): string
22+
{
23+
// @codeCoverageIgnoreStart
24+
$evaluator = new ConstExprEvaluator(function (Expr $expr) use ($contextStack) {
25+
return $this->evaluateFallback($expr, $contextStack);
26+
});
27+
28+
return $evaluator->evaluateSilently($expr);
29+
// @codeCoverageIgnoreEnd
30+
}
31+
32+
/** @throws ConstExprEvaluationException */
33+
private function evaluateFallback(Expr $expr, ContextStack $contextStack): string
34+
{
35+
$typeContext = $contextStack->getTypeContext();
36+
if ($typeContext === null) {
37+
throw new ConstExprEvaluationException(
38+
sprintf('Expression of type %s cannot be evaluated', $expr->getType())
39+
);
40+
}
41+
42+
if ($expr instanceof Node\Scalar\MagicConst\Namespace_) {
43+
return $typeContext->getNamespace();
44+
}
45+
46+
throw new ConstExprEvaluationException(
47+
sprintf('Expression of type %s cannot be evaluated', $expr->getType())
48+
);
49+
}
50+
}

tests/integration/ProjectCreationTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ public function testWithGlobalConstants() : void
206206
$this->assertArrayHasKey('\\Luigi\\OVEN_TEMPERATURE', $project->getFiles()[$fileName]->getConstants());
207207
$this->assertArrayHasKey('\\Luigi\\MAX_OVEN_TEMPERATURE', $project->getFiles()[$fileName]->getConstants());
208208
$this->assertArrayHasKey('\\OUTSIDE_OVEN_TEMPERATURE', $project->getFiles()[$fileName]->getConstants());
209+
$this->assertArrayHasKey('\\Luigi_OUTSIDE_OVEN_TEMPERATURE', $project->getFiles()[$fileName]->getConstants());
209210
}
210211

211212
public function testInterfaceExtends() : void

tests/integration/data/Luigi/constants.php

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
const OVEN_TEMPERATURE = 9001;
66
define('\\Luigi\\MAX_OVEN_TEMPERATURE', 9002);
77
define('OUTSIDE_OVEN_TEMPERATURE', 9002);
8+
define(__NAMESPACE__ . '_OUTSIDE_OVEN_TEMPERATURE', 9002);
9+
$v = 1;
10+
define($v . '_OUTSIDE_OVEN_TEMPERATURE', 9002);
811

912
function in_function_define(){
1013
define('IN_FUNCTION_OVEN_TEMPERATURE', 9003);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpDocumentor\Reflection\Php\ValueEvaluator;
6+
7+
use phpDocumentor\Reflection\Php\Factory\ContextStack;
8+
use phpDocumentor\Reflection\Php\Project;
9+
use phpDocumentor\Reflection\Types\Context;
10+
use PhpParser\ConstExprEvaluationException;
11+
use PhpParser\Node\Expr\ShellExec;
12+
use PhpParser\Node\Scalar\MagicConst\Namespace_;
13+
use PHPUnit\Framework\TestCase;
14+
15+
/**
16+
* @coversDefaultClass \phpDocumentor\Reflection\Php\ValueEvaluator\ConstantEvaluator
17+
*/
18+
final class ConstantEvaluatorTest extends TestCase
19+
{
20+
/** @covers ::evaluate */
21+
22+
/** @covers ::evaluateFallback */
23+
public function testEvaluateThrowsWhenTypeContextIsNotSet(): void
24+
{
25+
$this->expectException(ConstExprEvaluationException::class);
26+
27+
$evaluator = new ConstantEvaluator();
28+
$evaluator->evaluate(new Namespace_(), new ContextStack(new Project('test')));
29+
}
30+
31+
/** @covers ::evaluate */
32+
33+
/** @covers ::evaluateFallback */
34+
public function testEvaluateThrowsOnUnknownExpression(): void
35+
{
36+
$this->expectException(ConstExprEvaluationException::class);
37+
38+
$evaluator = new ConstantEvaluator();
39+
$result = $evaluator->evaluate(new ShellExec([]), new ContextStack(new Project('test'), new Context('Test')));
40+
}
41+
42+
/** @covers ::evaluate */
43+
44+
/** @covers ::evaluateFallback */
45+
public function testEvaluateReturnsNamespaceFromContext(): void
46+
{
47+
$evaluator = new ConstantEvaluator();
48+
$result = $evaluator->evaluate(new Namespace_(), new ContextStack(new Project('test'), new Context('Test')));
49+
50+
self::assertSame('Test', $result);
51+
}
52+
}

0 commit comments

Comments
 (0)