Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix easy cases of date related functions #341

Merged
merged 1 commit into from
Jun 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
File renamed without changes.
114 changes: 114 additions & 0 deletions specs/misc/date.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

/*
* This file is part of the humbug/php-scoper package.
*
* Copyright (c) 2017 Théo FIDRY <theo.fidry@gmail.com>,
* Pádraic Brady <padraic.brady@gmail.com>
*
* 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'
<?php

const ISO8601_BASIC = 'Ymd\THis\Z';

new Foo('d\H\Z');
new DateTime('d\H\Z');
new DateTimeImmutable('d\H\Z');
date_create('d\H\Z');
date('d\H\Z');
gmdate('d\H\Z');

DateTime::createFromFormat('d\H\Z', '15\Feb\2009');
DateTimeImmutable::createFromFormat('d\H\Z', '15\Feb\2009');
date_create_from_format('d\H\Z', '15\Feb\2009');

(new DateTime('now'))->format('d\H\Z');
date_format(new DateTime('now'), 'd\H\Z');

----
<?php

namespace Humbug;

const ISO8601_BASIC = 'Humbug\\Ymd\\THis\\Z';
new \Humbug\Foo('Humbug\\d\\H\\Z');
new \DateTime('d\\H\\Z');
new \DateTimeImmutable('d\\H\\Z');
\date_create('d\\H\\Z');
\date('d\\H\\Z');
\gmdate('d\\H\\Z');
\DateTime::createFromFormat('d\\H\\Z', '15\\Feb\\2009');
\DateTimeImmutable::createFromFormat('d\\H\\Z', '15\\Feb\\2009');
\date_create_from_format('d\\H\\Z', '15\\Feb\\2009');
(new \DateTime('now'))->format('Humbug\\d\\H\\Z');
\date_format(new \DateTime('now'), 'Humbug\\d\\H\\Z');

PHP
,

'date values in a namespace' => <<<'PHP'
<?php

namespace Acme;

use DateTime;
use DateTimeImmutable;

const ISO8601_BASIC = 'Ymd\THis\Z';

new Foo('d\H\Z');
new DateTime('d\H\Z');
new DateTimeImmutable('d\H\Z');
date_create('d\H\Z');
date('d\H\Z');
gmdate('d\H\Z');

DateTime::createFromFormat('d\H\Z', '15\Feb\2009');
DateTimeImmutable::createFromFormat('d\H\Z', '15\Feb\2009');
date_create_from_format('d\H\Z', '15\Feb\2009');

(new DateTime('now'))->format('d\H\Z');
date_format(new DateTime('now'), 'd\H\Z');

----
<?php

namespace Humbug\Acme;

use DateTime;
use DateTimeImmutable;
const ISO8601_BASIC = 'Humbug\\Ymd\\THis\\Z';
new \Humbug\Acme\Foo('Humbug\\d\\H\\Z');
new \DateTime('d\\H\\Z');
new \DateTimeImmutable('d\\H\\Z');
\date_create('d\\H\\Z');
\date('d\\H\\Z');
\gmdate('d\\H\\Z');
\DateTime::createFromFormat('d\\H\\Z', '15\\Feb\\2009');
\DateTimeImmutable::createFromFormat('d\\H\\Z', '15\\Feb\\2009');
\date_create_from_format('d\\H\\Z', '15\\Feb\\2009');
(new \DateTime('now'))->format('Humbug\\d\\H\\Z');
\date_format(new \DateTime('now'), 'Humbug\\d\\H\\Z');

PHP
,
];
File renamed without changes.
File renamed without changes.
File renamed without changes.
89 changes: 77 additions & 12 deletions src/PhpParser/NodeVisitor/StringScalarPrefixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -68,6 +72,11 @@ final class StringScalarPrefixer extends NodeVisitorAbstract
'trait_exists',
];

private const DATETIME_CLASSES = [
'datetime',
'datetimeimmutable',
];

private $prefix;
private $whitelist;
private $reflector;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down