Skip to content

Commit

Permalink
Add a fixer used to add ! prefix to the first argument of DateTime:…
Browse files Browse the repository at this point in the history
…:createFromFormat

Co-authored-by: Dariusz Rumiński <dariusz.ruminski@gmail.com>
Co-authored-by: Dariusz Rumiński <dariusz.ruminski@gmail.com>
  • Loading branch information
liquid207 and keradus committed Mar 11, 2022
1 parent d34ccf8 commit 2bad1a9
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 0 deletions.
12 changes: 12 additions & 0 deletions doc/list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,18 @@ List of Available Rules

`Source PhpCsFixer\\Fixer\\ControlStructure\\ControlStructureContinuationPositionFixer <./../src/Fixer/ControlStructure/ControlStructureContinuationPositionFixer.php>`_
- `date_time_create_from_format_call <./rules/function_notation/date_time_create_from_format_call.rst>`_

The first argument of ``DateTime::createFromFormat`` method must start with ``!``.

Consider this code:
``DateTime::createFromFormat('Y-m-d', '2022-02-11')``.
What value will be returned? '2022-01-11 00:00:00.0'? No, actual return
value has 'H:i:s' section like '2022-02-11 16:55:37.0'.
Change 'Y-m-d' to '!Y-m-d', return value will be '2022-01-11 00:00:00.0'.
So, adding ``!`` to format string will make return value more intuitive.

`Source PhpCsFixer\\Fixer\\FunctionNotation\\DateTimeCreateFromFormatCallFixer <./../src/Fixer/FunctionNotation/DateTimeCreateFromFormatCallFixer.php>`_
- `date_time_immutable <./rules/class_usage/date_time_immutable.rst>`_

Class ``DateTimeImmutable`` should be used instead of ``DateTime``.
Expand Down
29 changes: 29 additions & 0 deletions doc/rules/function_notation/date_time_create_from_format_call.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
==========================================
Rule ``date_time_create_from_format_call``
==========================================

The first argument of ``DateTime::createFromFormat`` method must start with
``!``.

Description
-----------

Consider this code:
``DateTime::createFromFormat('Y-m-d', '2022-02-11')``.
What value will be returned? '2022-01-11 00:00:00.0'? No, actual return
value has 'H:i:s' section like '2022-02-11 16:55:37.0'.
Change 'Y-m-d' to '!Y-m-d', return value will be '2022-01-11 00:00:00.0'.
So, adding ``!`` to format string will make return value more intuitive.

Examples
--------

Example #1
~~~~~~~~~~

.. code-block:: diff
--- Original
+++ New
-<?php \DateTime::createFromFormat('Y-m-d', '2022-02-11');
+<?php \DateTime::createFromFormat('!Y-m-d', '2022-02-11');
3 changes: 3 additions & 0 deletions doc/rules/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ Function Notation
- `combine_nested_dirname <./function_notation/combine_nested_dirname.rst>`_ *(risky)*

Replace multiple nested calls of ``dirname`` by only one call with second ``$level`` parameter. Requires PHP >= 7.0.
- `date_time_create_from_format_call <./function_notation/date_time_create_from_format_call.rst>`_

The first argument of ``DateTime::createFromFormat`` method must start with ``!``.
- `fopen_flag_order <./function_notation/fopen_flag_order.rst>`_ *(risky)*

Order the flags in ``fopen`` calls, ``b`` and ``t`` must be last.
Expand Down
138 changes: 138 additions & 0 deletions src/Fixer/FunctionNotation/DateTimeCreateFromFormatCallFixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

declare(strict_types=1);

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Fixer\FunctionNotation;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

final class DateTimeCreateFromFormatCallFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'The first argument of `DateTime::createFromFormat` method must start with `!`.',
[
new CodeSample("<?php \\DateTime::createFromFormat('Y-m-d', '2022-02-11');\n"),
],
"Consider this code:
`DateTime::createFromFormat('Y-m-d', '2022-02-11')`.
What value will be returned? '2022-01-11 00:00:00.0'? No, actual return value has 'H:i:s' section like '2022-02-11 16:55:37.0'.
Change 'Y-m-d' to '!Y-m-d', return value will be '2022-01-11 00:00:00.0'.
So, adding `!` to format string will make return value more intuitive."
);
}

public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_DOUBLE_COLON);
}

protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$argumentsAnalyzer = new ArgumentsAnalyzer();
$namespacesAnalyzer = new NamespacesAnalyzer();
$namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();

foreach ($namespacesAnalyzer->getDeclarations($tokens) as $namespace) {
$scopeStartIndex = $namespace->getScopeStartIndex();
$useDeclarations = $namespaceUsesAnalyzer->getDeclarationsInNamespace($tokens, $namespace);

for ($index = $namespace->getScopeEndIndex(); $index > $scopeStartIndex; --$index) {
if (!$tokens[$index]->isGivenKind(T_DOUBLE_COLON)) {
continue;
}

$functionNameIndex = $tokens->getNextMeaningfulToken($index);

if (!$tokens[$functionNameIndex]->equals([T_STRING, 'createFromFormat'], false)) {
continue;
}

if (!$tokens[$tokens->getNextMeaningfulToken($functionNameIndex)]->equals('(')) {
continue;
}

$classNameIndex = $tokens->getPrevMeaningfulToken($index);

if (!$tokens[$classNameIndex]->equals([T_STRING, 'DateTime'], false)) {
continue;
}

$preClassNameIndex = $tokens->getPrevMeaningfulToken($classNameIndex);

if ($tokens[$preClassNameIndex]->isGivenKind(T_NS_SEPARATOR)) {
if ($tokens[$tokens->getPrevMeaningfulToken($preClassNameIndex)]->isGivenKind(T_STRING)) {
continue;
}
} elseif (!$namespace->isGlobalNamespace()) {
continue;
} else {
foreach ($useDeclarations as $useDeclaration) {
if ('datetime' === strtolower($useDeclaration->getShortName()) && 'datetime' !== strtolower($useDeclaration->getFullName())) {
continue 2;
}
}
}

$openIndex = $tokens->getNextTokenOfKind($functionNameIndex, ['(']);
$closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);

$argumentIndex = $this->getFirstArgumentTokenIndex($tokens, $argumentsAnalyzer->getArguments($tokens, $openIndex, $closeIndex));

if (null === $argumentIndex) {
continue;
}

$format = $tokens[$argumentIndex]->getContent();

if ('!' === substr($format, 1, 1)) {
continue;
}

$tokens->clearAt($argumentIndex);
$tokens->insertAt($argumentIndex, new Token([T_CONSTANT_ENCAPSED_STRING, substr_replace($format, '!', 1, 0)]));
}
}
}

private function getFirstArgumentTokenIndex(Tokens $tokens, array $arguments): ?int
{
if (2 !== \count($arguments)) {
return null;
}

$argumentStartIndex = array_key_first($arguments);
$argumentEndIndex = $arguments[$argumentStartIndex];
$argumentStartIndex = $tokens->getNextMeaningfulToken($argumentStartIndex - 1);

if (
$argumentStartIndex !== $argumentEndIndex
&& $tokens->getNextMeaningfulToken($argumentStartIndex) <= $argumentEndIndex
) {
return null; // argument is not a simple single string
}

return !$tokens[$argumentStartIndex]->isGivenKind(T_CONSTANT_ENCAPSED_STRING)
? null // first argument is not a string
: $argumentStartIndex;
}
}
125 changes: 125 additions & 0 deletions tests/Fixer/FunctionNotation/DateTimeCreateFromFormatCallFixerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

declare(strict_types=1);

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Tests\Fixer\FunctionNotation;

use PhpCsFixer\Tests\Test\AbstractFixerTestCase;

/**
* @internal
* @covers \PhpCsFixer\Fixer\FunctionNotation\DateTimeCreateFromFormatCallFixer
*/
final class DateTimeCreateFromFormatCallFixerTest extends AbstractFixerTestCase
{
/**
* @dataProvider provideFixCases
*/
public function testFix(string $expected, ?string $input = null): void
{
$this->doTest($expected, $input);
}

public function provideFixCases(): \Generator
{
yield [
'<?php \DateTime::createFromFormat(\'!Y-m-d\', \'2022-02-11\');',
'<?php \DateTime::createFromFormat(\'Y-m-d\', \'2022-02-11\');',
];

yield [
'<?php use DateTime; DateTime::createFromFormat(\'!Y-m-d\', \'2022-02-11\');',
'<?php use DateTime; DateTime::createFromFormat(\'Y-m-d\', \'2022-02-11\');',
];

yield [
'<?php DateTime::createFromFormat(\'!Y-m-d\', \'2022-02-11\');',
'<?php DateTime::createFromFormat(\'Y-m-d\', \'2022-02-11\');',
];

yield [
'<?php use \Example\DateTime; DateTime::createFromFormat(\'Y-m-d\', \'2022-02-11\');',
];

yield [
'<?php use \Example\datetime; DATETIME::createFromFormat(\'Y-m-d\', \'2022-02-11\');',
];

yield [
'<?php \DateTime::createFromFormat("!Y-m-d", \'2022-02-11\');',
'<?php \DateTime::createFromFormat("Y-m-d", \'2022-02-11\');',
];

yield [
'<?php \DateTime::createFromFormat($foo, \'2022-02-11\');',
];

yield [
'<?php \DATETIME::createFromFormat( "!Y-m-d", \'2022-02-11\');',
'<?php \DATETIME::createFromFormat( "Y-m-d", \'2022-02-11\');',
];

yield [
'<?php \DateTime::createFromFormat(/* aaa */ \'!Y-m-d\', \'2022-02-11\');',
'<?php \DateTime::createFromFormat(/* aaa */ \'Y-m-d\', \'2022-02-11\');',
];

yield [
'<?php /*1*//*2*/DateTime/*3*/::/*4*/createFromFormat/*5*/(/*6*/"!Y-m-d"/*7*/,/*8*/"2022-02-11"/*9*/)/*10*/ ?>',
'<?php /*1*//*2*/DateTime/*3*/::/*4*/createFromFormat/*5*/(/*6*/"Y-m-d"/*7*/,/*8*/"2022-02-11"/*9*/)/*10*/ ?>',
];

yield [
'<?php \DateTime::createFromFormat(\'Y-m-d\');',
];

yield [
'<?php \DateTime::createFromFormat($a, $b);',
];

yield [
'<?php \DateTime::createFromFormat(\'Y-m-d\', $b, $c);',
];

yield [
'<?php A\DateTime::createFromFormat(\'Y-m-d\', \'2022-02-11\');',
];

yield [
'<?php A\DateTime::createFromFormat(\'Y-m-d\'."a", \'2022-02-11\');',
];

yield ['<?php \DateTime::createFromFormat(123, \'2022-02-11\');'];

yield [
'<?php namespace {
\DateTime::createFromFormat(\'!Y-m-d\', \'2022-02-11\');
}
namespace Bar {
class DateTime extends Foo {}
DateTime::createFromFormat(\'Y-m-d\', \'2022-02-11\');
}
',
'<?php namespace {
\DateTime::createFromFormat(\'Y-m-d\', \'2022-02-11\');
}
namespace Bar {
class DateTime extends Foo {}
DateTime::createFromFormat(\'Y-m-d\', \'2022-02-11\');
}
',
];
}
}

0 comments on commit 2bad1a9

Please sign in to comment.