Skip to content

Commit

Permalink
Merge pull request #245 from phpDocumentor/feature/182_define_express…
Browse files Browse the repository at this point in the history
…ions

Add expression evaluation for define names.
  • Loading branch information
jaapio committed Apr 2, 2022
2 parents 4f5a896 + c409391 commit 936e4dd
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 18 deletions.
Expand Up @@ -20,7 +20,7 @@
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use Webmozart\Assert\Assert;

class ConstructorPromotion extends AbstractFactory
final class ConstructorPromotion extends AbstractFactory
{
/** @var PrettyPrinter */
private $valueConverter;
Expand Down
54 changes: 37 additions & 17 deletions src/phpDocumentor/Reflection/Php/Factory/Define.php
Expand Up @@ -19,14 +19,15 @@
use phpDocumentor\Reflection\Php\Constant as ConstantElement;
use phpDocumentor\Reflection\Php\File as FileElement;
use phpDocumentor\Reflection\Php\StrategyContainer;
use phpDocumentor\Reflection\Php\ValueEvaluator\ConstantEvaluator;
use PhpParser\ConstExprEvaluationException;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\VariadicPlaceholder;
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use RuntimeException;

use function assert;
use function sprintf;
Expand All @@ -43,13 +44,20 @@ final class Define extends AbstractFactory
/** @var PrettyPrinter */
private $valueConverter;

/** @var ConstantEvaluator */
private $constantEvaluator;

/**
* Initializes the object.
*/
public function __construct(DocBlockFactoryInterface $docBlockFactory, PrettyPrinter $prettyPrinter)
{
public function __construct(
DocBlockFactoryInterface $docBlockFactory,
PrettyPrinter $prettyPrinter,
?ConstantEvaluator $constantEvaluator = null
) {
parent::__construct($docBlockFactory);
$this->valueConverter = $prettyPrinter;
$this->constantEvaluator = $constantEvaluator ?? new ConstantEvaluator();
}

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

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

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

$fqsen = $this->determineFqsen($name, $context);
if ($fqsen === null) {
return;
}

$constant = new ConstantElement(
$this->determineFqsen($name),
$fqsen,
$this->createDocBlock($object->getDocComment(), $context->getTypeContext()),
$this->determineValue($value),
new Location($object->getLine()),
Expand All @@ -122,15 +130,27 @@ private function determineValue(?Arg $value): ?string
return $this->valueConverter->prettyPrintExpr($value->value);
}

private function determineFqsen(Arg $name): Fqsen
private function determineFqsen(Arg $name, ContextStack $context): ?Fqsen
{
$nameString = $name->value;
assert($nameString instanceof String_);
return $this->fqsenFromExpression($name->value, $context);
}

if (strpos($nameString->value, '\\') === false) {
return new Fqsen(sprintf('\\%s', $nameString->value));
private function fqsenFromExpression(Expr $nameString, ContextStack $context): ?Fqsen
{
try {
return $this->fqsenFromString($this->constantEvaluator->evaluate($nameString, $context));
} catch (ConstExprEvaluationException $e) {
//Ignore any errors as we cannot evaluate all expressions
return null;
}
}

private function fqsenFromString(string $nameString): Fqsen
{
if (strpos($nameString, '\\') === false) {
return new Fqsen(sprintf('\\%s', $nameString));
}

return new Fqsen(sprintf('%s', $nameString->value));
return new Fqsen(sprintf('%s', $nameString));
}
}
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace phpDocumentor\Reflection\Php\ValueEvaluator;

use phpDocumentor\Reflection\Php\Factory\ContextStack;
use PhpParser\ConstExprEvaluationException;
use PhpParser\ConstExprEvaluator;
use PhpParser\Node;
use PhpParser\Node\Expr;

use function sprintf;

/**
* @internal
*/
final class ConstantEvaluator
{
/** @throws ConstExprEvaluationException */
public function evaluate(Expr $expr, ContextStack $contextStack): string
{
// @codeCoverageIgnoreStart
$evaluator = new ConstExprEvaluator(function (Expr $expr) use ($contextStack) {
return $this->evaluateFallback($expr, $contextStack);
});

return $evaluator->evaluateSilently($expr);
// @codeCoverageIgnoreEnd
}

/** @throws ConstExprEvaluationException */
private function evaluateFallback(Expr $expr, ContextStack $contextStack): string
{
$typeContext = $contextStack->getTypeContext();
if ($typeContext === null) {
throw new ConstExprEvaluationException(
sprintf('Expression of type %s cannot be evaluated', $expr->getType())
);
}

if ($expr instanceof Node\Scalar\MagicConst\Namespace_) {
return $typeContext->getNamespace();
}

throw new ConstExprEvaluationException(
sprintf('Expression of type %s cannot be evaluated', $expr->getType())
);
}
}
1 change: 1 addition & 0 deletions tests/integration/ProjectCreationTest.php
Expand Up @@ -206,6 +206,7 @@ public function testWithGlobalConstants() : void
$this->assertArrayHasKey('\\Luigi\\OVEN_TEMPERATURE', $project->getFiles()[$fileName]->getConstants());
$this->assertArrayHasKey('\\Luigi\\MAX_OVEN_TEMPERATURE', $project->getFiles()[$fileName]->getConstants());
$this->assertArrayHasKey('\\OUTSIDE_OVEN_TEMPERATURE', $project->getFiles()[$fileName]->getConstants());
$this->assertArrayHasKey('\\Luigi_OUTSIDE_OVEN_TEMPERATURE', $project->getFiles()[$fileName]->getConstants());
}

public function testInterfaceExtends() : void
Expand Down
3 changes: 3 additions & 0 deletions tests/integration/data/Luigi/constants.php
Expand Up @@ -5,6 +5,9 @@
const OVEN_TEMPERATURE = 9001;
define('\\Luigi\\MAX_OVEN_TEMPERATURE', 9002);
define('OUTSIDE_OVEN_TEMPERATURE', 9002);
define(__NAMESPACE__ . '_OUTSIDE_OVEN_TEMPERATURE', 9002);
$v = 1;
define($v . '_OUTSIDE_OVEN_TEMPERATURE', 9002);

function in_function_define(){
define('IN_FUNCTION_OVEN_TEMPERATURE', 9003);
Expand Down
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace phpDocumentor\Reflection\Php\ValueEvaluator;

use phpDocumentor\Reflection\Php\Factory\ContextStack;
use phpDocumentor\Reflection\Php\Project;
use phpDocumentor\Reflection\Types\Context;
use PhpParser\ConstExprEvaluationException;
use PhpParser\Node\Expr\ShellExec;
use PhpParser\Node\Scalar\MagicConst\Namespace_;
use PHPUnit\Framework\TestCase;

/**
* @coversDefaultClass \phpDocumentor\Reflection\Php\ValueEvaluator\ConstantEvaluator
*/
final class ConstantEvaluatorTest extends TestCase
{
/** @covers ::evaluate */

/** @covers ::evaluateFallback */
public function testEvaluateThrowsWhenTypeContextIsNotSet(): void
{
$this->expectException(ConstExprEvaluationException::class);

$evaluator = new ConstantEvaluator();
$evaluator->evaluate(new Namespace_(), new ContextStack(new Project('test')));
}

/** @covers ::evaluate */

/** @covers ::evaluateFallback */
public function testEvaluateThrowsOnUnknownExpression(): void
{
$this->expectException(ConstExprEvaluationException::class);

$evaluator = new ConstantEvaluator();
$result = $evaluator->evaluate(new ShellExec([]), new ContextStack(new Project('test'), new Context('Test')));
}

/** @covers ::evaluate */

/** @covers ::evaluateFallback */
public function testEvaluateReturnsNamespaceFromContext(): void
{
$evaluator = new ConstantEvaluator();
$result = $evaluator->evaluate(new Namespace_(), new ContextStack(new Project('test'), new Context('Test')));

self::assertSame('Test', $result);
}
}

0 comments on commit 936e4dd

Please sign in to comment.