From c409391f726ec34704d85cadea0298647635c0fe Mon Sep 17 00:00:00 2001 From: Jaapio Date: Sat, 2 Apr 2022 21:48:03 +0200 Subject: [PATCH] Add expression evaluation for define names. Allow semi dynamic defines. Using a minimal implementation of expression evaluation. fixes #182, #237 --- .../Php/Factory/ConstructorPromotion.php | 2 +- .../Reflection/Php/Factory/Define.php | 54 +++++++++++++------ .../Php/ValueEvaluator/ConstantEvaluator.php | 50 +++++++++++++++++ tests/integration/ProjectCreationTest.php | 1 + tests/integration/data/Luigi/constants.php | 3 ++ .../ValueEvaluator/ConstantEvaluatorTest.php | 52 ++++++++++++++++++ 6 files changed, 144 insertions(+), 18 deletions(-) create mode 100644 src/phpDocumentor/Reflection/Php/ValueEvaluator/ConstantEvaluator.php create mode 100644 tests/unit/phpDocumentor/Reflection/Php/ValueEvaluator/ConstantEvaluatorTest.php diff --git a/src/phpDocumentor/Reflection/Php/Factory/ConstructorPromotion.php b/src/phpDocumentor/Reflection/Php/Factory/ConstructorPromotion.php index 28512cd6..e9b14b37 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/ConstructorPromotion.php +++ b/src/phpDocumentor/Reflection/Php/Factory/ConstructorPromotion.php @@ -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; diff --git a/src/phpDocumentor/Reflection/Php/Factory/Define.php b/src/phpDocumentor/Reflection/Php/Factory/Define.php index bf6d7860..c42bbdf6 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/Define.php +++ b/src/phpDocumentor/Reflection/Php/Factory/Define.php @@ -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; @@ -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 @@ -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; @@ -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()), @@ -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)); } } diff --git a/src/phpDocumentor/Reflection/Php/ValueEvaluator/ConstantEvaluator.php b/src/phpDocumentor/Reflection/Php/ValueEvaluator/ConstantEvaluator.php new file mode 100644 index 00000000..fa03902a --- /dev/null +++ b/src/phpDocumentor/Reflection/Php/ValueEvaluator/ConstantEvaluator.php @@ -0,0 +1,50 @@ +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()) + ); + } +} diff --git a/tests/integration/ProjectCreationTest.php b/tests/integration/ProjectCreationTest.php index a9e5df5b..d97656fb 100644 --- a/tests/integration/ProjectCreationTest.php +++ b/tests/integration/ProjectCreationTest.php @@ -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 diff --git a/tests/integration/data/Luigi/constants.php b/tests/integration/data/Luigi/constants.php index d3afaca3..5d7574b9 100644 --- a/tests/integration/data/Luigi/constants.php +++ b/tests/integration/data/Luigi/constants.php @@ -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); diff --git a/tests/unit/phpDocumentor/Reflection/Php/ValueEvaluator/ConstantEvaluatorTest.php b/tests/unit/phpDocumentor/Reflection/Php/ValueEvaluator/ConstantEvaluatorTest.php new file mode 100644 index 00000000..91a7db48 --- /dev/null +++ b/tests/unit/phpDocumentor/Reflection/Php/ValueEvaluator/ConstantEvaluatorTest.php @@ -0,0 +1,52 @@ +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); + } +}