From e431a8a8061dc849031a26e910fb11b0548bc59b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Mon, 10 Jun 2019 08:02:22 +0200 Subject: [PATCH] Fix easy cases of date related functions Closes #301 --- README.md | 14 +++ specs/{ => misc}/class-FQ.php | 0 specs/misc/date.php | 114 ++++++++++++++++++ specs/{ => misc}/eval.php | 0 specs/{ => misc}/misc.php | 0 specs/{ => misc}/nowdoc.php | 0 .../whitelist-case-sensitiveness.php | 0 .../NodeVisitor/StringScalarPrefixer.php | 89 ++++++++++++-- 8 files changed, 205 insertions(+), 12 deletions(-) rename specs/{ => misc}/class-FQ.php (100%) create mode 100644 specs/misc/date.php rename specs/{ => misc}/eval.php (100%) rename specs/{ => misc}/misc.php (100%) rename specs/{ => misc}/nowdoc.php (100%) rename specs/{ => misc}/whitelist-case-sensitiveness.php (100%) diff --git a/README.md b/README.md index 591a9225..c8ca21df 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ potentially very difficult to debug due to dissimilar or unsupported package ver - [Recommendations](#recommendations) - [Limitations](#limitations) - [Dynamic symbols](#dynamic-symbols) + - [Date symbols](#date-symbols) - [Heredoc values](#heredoc-values) - [Callables](#callables) - [String values](#string-values) @@ -648,6 +649,19 @@ possible such as: - `const X = 'Symfony\\Component' . '\\Yaml\\Ya_1';` +### Date symbols + +You code may be using a convention for the date string formats which could be mistaken for classes, e.g.: + +```php +const ISO8601_BASIC = 'Ymd\THis\Z'; +``` + +In this scenario, PHP-Scoper has no way to tell that string `'Ymd\THis\Z'` does not refer to a symbol but is +a date format. In this case, you will have to rely on patchers. Note however that PHP-Scoper will be able to +handle some cases such as, see the [date-spec](specs/misc/date.php). + + ### Heredoc values If you consider the following code: diff --git a/specs/class-FQ.php b/specs/misc/class-FQ.php similarity index 100% rename from specs/class-FQ.php rename to specs/misc/class-FQ.php diff --git a/specs/misc/date.php b/specs/misc/date.php new file mode 100644 index 00000000..faf18a70 --- /dev/null +++ b/specs/misc/date.php @@ -0,0 +1,114 @@ +, + * Pádraic Brady + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + 'meta' => [ + 'title' => 'Date related functions/calls', + // Default values. If not specified will be the one used + 'prefix' => 'Humbug', + 'whitelist' => [], + 'whitelist-global-constants' => true, + 'whitelist-global-classes' => false, + 'whitelist-global-functions' => true, + 'registered-classes' => [], + 'registered-functions' => [], + ], + + 'date values' => <<<'PHP' +format('d\H\Z'); +date_format(new DateTime('now'), 'd\H\Z'); + +---- +format('Humbug\\d\\H\\Z'); +\date_format(new \DateTime('now'), 'Humbug\\d\\H\\Z'); + +PHP + , + + 'date values in a namespace' => <<<'PHP' +format('d\H\Z'); +date_format(new DateTime('now'), 'd\H\Z'); + +---- +format('Humbug\\d\\H\\Z'); +\date_format(new \DateTime('now'), 'Humbug\\d\\H\\Z'); + +PHP + , +]; diff --git a/specs/eval.php b/specs/misc/eval.php similarity index 100% rename from specs/eval.php rename to specs/misc/eval.php diff --git a/specs/misc.php b/specs/misc/misc.php similarity index 100% rename from specs/misc.php rename to specs/misc/misc.php diff --git a/specs/nowdoc.php b/specs/misc/nowdoc.php similarity index 100% rename from specs/nowdoc.php rename to specs/misc/nowdoc.php diff --git a/specs/whitelist-case-sensitiveness.php b/specs/misc/whitelist-case-sensitiveness.php similarity index 100% rename from specs/whitelist-case-sensitiveness.php rename to specs/misc/whitelist-case-sensitiveness.php diff --git a/src/PhpParser/NodeVisitor/StringScalarPrefixer.php b/src/PhpParser/NodeVisitor/StringScalarPrefixer.php index 71ad2ab6..e2d2d137 100644 --- a/src/PhpParser/NodeVisitor/StringScalarPrefixer.php +++ b/src/PhpParser/NodeVisitor/StringScalarPrefixer.php @@ -23,6 +23,9 @@ use PhpParser\Node\Expr\ArrayItem; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\New_; +use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Param; @@ -38,6 +41,7 @@ use function is_string; use function preg_match; use function strpos; +use function strtolower; /** * Prefixes the string scalar values when appropriate. @@ -68,6 +72,11 @@ final class StringScalarPrefixer extends NodeVisitorAbstract 'trait_exists', ]; + private const DATETIME_CLASSES = [ + 'datetime', + 'datetimeimmutable', + ]; + private $prefix; private $whitelist; private $reflector; @@ -138,26 +147,52 @@ private function prefixStringScalar(String_ $string): String_ private function prefixStringArg(String_ $string, Arg $parentNode): String_ { - $functionNode = ParentNodeAppender::getParent($parentNode); + $callerNode = ParentNodeAppender::getParent($parentNode); - if (false === ($functionNode instanceof FuncCall)) { - // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string - return $this->belongsToTheGlobalNamespace($string) - ? $string - : $this->createPrefixedString($string) - ; + if ($callerNode instanceof New_) { + return $this->prefixNewStringArg($string, $callerNode); + } + + if ($callerNode instanceof FuncCall) { + return $this->prefixFunctionStringArg($string, $callerNode); + } + + if ($callerNode instanceof StaticCall) { + return $this->prefixStaticCallStringArg($string, $callerNode); } - /** @var FuncCall $functionNode */ + // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular + // string + return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string); + } + + private function prefixNewStringArg(String_ $string, New_ $newNode): String_ + { + $class = $newNode->class; + + if (false === ($class instanceof FullyQualified)) { + return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string); + } + + if (in_array(strtolower($class->toString()), self::DATETIME_CLASSES, true)) { + return $string; + } + + return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string); + } + + private function prefixFunctionStringArg(String_ $string, FuncCall $functionNode): String_ + { // In the case of a function call, we allow to prefix strings which could be classes belonging to the global // namespace in some cases $functionName = $functionNode->name instanceof Name ? (string) $functionNode->name : null; + if (in_array($functionName, ['date_create', 'date', 'gmdate', 'date_create_from_format'], true)) { + return $string; + } + if (false === in_array($functionName, self::SPECIAL_FUNCTION_NAMES, true)) { - return $this->belongsToTheGlobalNamespace($string) - ? $string - : $this->createPrefixedString($string) - ; + return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string); } if ('function_exists' === $functionName) { @@ -193,6 +228,27 @@ private function prefixStringArg(String_ $string, Arg $parentNode): String_ ; } + private function prefixStaticCallStringArg(String_ $string, StaticCall $callNode): String_ + { + $class = $callNode->class; + + if (false === ($class instanceof FullyQualified)) { + return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string); + } + + if (false === in_array(strtolower($class->toString()), self::DATETIME_CLASSES, true)) { + return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string); + } + + if ($callNode->name instanceof Identifier + && 'createFromFormat' === $callNode->name->toString() + ) { + return $string; + } + + return $this->createPrefixedStringIfDoesNotBelongToGlobalNamespace($string); + } + private function prefixArrayItemString(String_ $string, ArrayItem $parentNode): String_ { // ArrayItem can lead to two results: either the string is used for `spl_autoload_register()`, e.g. @@ -269,6 +325,15 @@ private function isConstantNode(String_ $node): bool return $parent === $argParent->args[0]; } + private function createPrefixedStringIfDoesNotBelongToGlobalNamespace(String_ $string): String_ + { + // If belongs to the global namespace then we cannot differentiate the value from a symbol and a regular string + return $this->belongsToTheGlobalNamespace($string) + ? $string + : $this->createPrefixedString($string) + ; + } + private function createPrefixedString(String_ $previous): String_ { $previousValueParts = array_values(