Repository: sabbelasichon/typo3-rector (php)

Issue:
Feature: #97193 - New TCA type "number"
Feature: #97193 - New TCA type "number"

https://docs.typo3.org/c/typo3/cms-core/master/en-us/Changelog/12.0/Feature-97193-NewTCATypeNumber.html
.. include:: /Includes.rst.txt

.. _feature-97193:

=======================================
Feature: #97193 - New TCA type "number"
=======================================

See :issue:`97193`

Description
===========

Especially TCA type :php:`input` has a wide range of use cases, depending
on the configured :php:`renderType` and the :php:`eval` options. Determination
of the semantic meaning is therefore usually quite hard and often leads to
duplicated checks and evaluations in custom extension code.

In our effort of introducing dedicated TCA types for all those use
cases, the TCA type :php:`number` has been introduced. It replaces the
:php:`eval=int` and :php:`eval=double2` options of TCA type :php:`input`.

The TCA :php:`number` fields will be rendered with the html :html:`type`
attribute set to :html:`number`.

The TCA type :php:`number` features the following column configuration:

- :php:`autocomplete`
- :php:`behaviour`: :php:`allowLanguageSynchronization`
- :php:`default`
- :php:`fieldControl`
- :php:`fieldInformation`
- :php:`fieldWizard`
- :php:`format`: :php:`integer`, :php:`decimal`
- :php:`mode`
- :php:`nullable`
- :php:`placeholder`
- :php:`range`: :php:`lower`, :php:`upper`
- :php:`readOnly`
- :php:`required`
- :php:`search`
- :php:`size`
- :php:`slider`: :php:`step`, :php:`width`
- :php:`valuePicker`: :php:`items`, :php:`mode`

The following column configuration can be overwritten by page TSconfig:

- :typoscript:`readOnly`
- :typoscript:`size`

The TCA type :php:`number` introduces the new configuration :php:`format`,
which can be set to :php:`decimal` or :php:`integer`, which is the default.

.. note::

   The :php:`slider` option allows to define a visual slider element
   next to the input field. The steps can be defined with the :php:`step`
   option. The minimum and maximum value can be configured with the
   :php:`range[lower]` and :php:`range[upper]` options.

.. note::

   The :php:`valuePicker` option allows to define default values via
   :php:`items`. With :php:`mode`, one can define how the selected
   value should be added (replace, prepend or append).

.. note::

    The options :php:`range`, :php:`slider` as well as :php:`eval=double2`
    are no longer evaluated for TCA type :php:`input`.

Migration
---------

The migration from :php:`eval='int'` to :php:`type=number`
is done like following:

..  code-block:: php

    // Before

    'int_field' => [
        'label' => 'Int field',
        'config' => [
            'type' => 'input',
            'eval' => 'int',
        ]
    ]

    // After

    'int_field' => [
        'label' => 'Int field',
        'config' => [
            'type' => 'number',
        ]
    ]

The migration from :php:`eval=double2` to :php:`type=number`
is done like following:

..  code-block:: php

    // Before

    'double2_field' => [
        'label' => 'double2 field',
        'config' => [
            'type' => 'input',
            'eval' => 'double2',
        ]
    ]

    // After

    'double2_field' => [
        'label' => 'double2 field',
        'config' => [
            'type' => 'number',
            'format' => 'decimal'
        ]
    ]

An automatic TCA migration is performed on the fly, migrating all occurrences
to the new TCA type and triggering a PHP :php:`E_USER_DEPRECATED` error
where code adoption has to take place.

Impact
======

It's now possible to simplify the TCA configuration by using the new
dedicated TCA type :php:`number`. Setting the new :php:`format` option
to :php:`decimal` behaves like the former :php:`eval=double2`.

.. index:: TCA, ext:backend


Pull request:
[TASK] Add MigrateEvalIntAndDouble2ToTypeNumberRector
Resolves #3552

Patch:
diff --git a/config/v12/flexform-120.php b/config/v12/flexform-120.php
index 038c8b14..826bffb2 100644
--- a/config/v12/flexform-120.php
+++ b/config/v12/flexform-120.php
@@ -3,9 +3,11 @@
 declare(strict_types=1);
 
 use Rector\Config\RectorConfig;
+use Ssch\TYPO3Rector\Rector\v12\v0\flexform\MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector;
 use Ssch\TYPO3Rector\Rector\v12\v0\flexform\RemoveElementTceFormsRector;
 
 return static function (RectorConfig $rectorConfig): void {
     $rectorConfig->import(__DIR__ . '/../config.php');
     $rectorConfig->rule(RemoveElementTceFormsRector::class);
+    $rectorConfig->rule(MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector::class);
 };
diff --git a/config/v12/tca-120.php b/config/v12/tca-120.php
index 2a2949ee..bac57a49 100644
--- a/config/v12/tca-120.php
+++ b/config/v12/tca-120.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 use Rector\Config\RectorConfig;
 use Ssch\TYPO3Rector\Rector\v12\v0\tca\MigrateColsToSizeForTcaTypeNoneRector;
+use Ssch\TYPO3Rector\Rector\v12\v0\tca\MigrateEvalIntAndDouble2ToTypeNumberRector;
 use Ssch\TYPO3Rector\Rector\v12\v0\tca\MigrateInputDateTimeRector;
 use Ssch\TYPO3Rector\Rector\v12\v0\tca\MigrateInternalTypeRector;
 use Ssch\TYPO3Rector\Rector\v12\v0\tca\MigrateNullFlagRector;
@@ -24,4 +25,5 @@ return static function (RectorConfig $rectorConfig): void {
     $rectorConfig->rule(RemoveTCAInterfaceAlwaysDescriptionRector::class);
     $rectorConfig->rule(RemoveCruserIdRector::class);
     $rectorConfig->rule(RemoveTableLocalPropertyRector::class);
+    $rectorConfig->rule(MigrateEvalIntAndDouble2ToTypeNumberRector::class);
 };
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 00000000..aab49911
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,2 @@
+parameters:
+	ignoreErrors: []
diff --git a/phpstan.neon b/phpstan.neon
index 293cd903..17de5a03 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,4 +1,5 @@
 includes:
+    - phpstan-baseline.neon
     # see https://github.com/symplify/phpstan-extensions
     - utils/phpstan/config/typo3-rector.neon
 
@@ -33,12 +34,17 @@ parameters:
          - tests/bootstrap-phpstan.php
 
     inferPrivatePropertyTypeFromConstructor: true
-    reportUnmatchedIgnoredErrors: true
+    reportUnmatchedIgnoredErrors: false
 
     ignoreErrors:
         # node finder
         - '#Method Ssch\\TYPO3Rector\\Rector\\v11\\v0\\ForwardResponseInsteadOfForwardMethodRector\:\:extractForwardMethodCalls\(\) should return array<PhpParser\\Node\\Expr\\MethodCall\> but returns array<PhpParser\\Node\>#'
 
+        # These 3 rules are not found for some strange reasons in GitHub Actions but locally they are reported
+        - "#\\:\\:getRuleDefinition\\(\\) has invalid return type Symplify\\\\RuleDocGenerator\\\\ValueObject\\\\RuleDefinition\\.$#"
+        - "#^Instantiated class Symplify\\\\RuleDocGenerator\\\\ValueObject\\\\CodeSample\\\\CodeSample not found\\.$#"
+        - "#^Instantiated class Symplify\\\\RuleDocGenerator\\\\ValueObject\\\\RuleDefinition not found\\.$#"
+
         # waits for Rector and PHPStan to share same php-parser, probably with PHPStan 1.0
         - '#PhpParser\\Node\\Arg\|PhpParser\\Node\\VariadicPlaceholder#'
 
diff --git a/src/FileProcessor/FlexForms/FlexFormsProcessor.php b/src/FileProcessor/FlexForms/FlexFormsProcessor.php
index 8d0642ca..0584d1b0 100644
--- a/src/FileProcessor/FlexForms/FlexFormsProcessor.php
+++ b/src/FileProcessor/FlexForms/FlexFormsProcessor.php
@@ -52,6 +52,7 @@ final class FlexFormsProcessor implements FileProcessorInterface
         $oldFileContents = $file->getFileContent();
 
         $domDocument = new DOMDocument();
+        $domDocument->preserveWhiteSpace = true;
         $domDocument->formatOutput = true;
         $domDocument->loadXML($oldFileContents);
 
@@ -69,6 +70,10 @@ final class FlexFormsProcessor implements FileProcessorInterface
             throw new UnexpectedValueException('Could not convert to xml');
         }
 
+        // Remove empty lines
+        /** @see https://stackoverflow.com/a/709684 */
+        $xml = preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $xml);
+
         // add end of line
         $xml .= PHP_EOL;
 
diff --git a/src/Helper/FlexFormHelperTrait.php b/src/Helper/FlexFormHelperTrait.php
new file mode 100644
index 00000000..2326c065
--- /dev/null
+++ b/src/Helper/FlexFormHelperTrait.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Ssch\TYPO3Rector\Helper;
+
+use DOMElement;
+
+trait FlexFormHelperTrait
+{
+    protected function extractDomElementByKey(?DOMElement $element, string $key): ?DOMElement
+    {
+        if (! $element instanceof DOMElement) {
+            return null;
+        }
+
+        foreach ($element->childNodes as $childNode) {
+            if (! $childNode instanceof DOMElement) {
+                continue;
+            }
+
+            $itemKey = (string) $childNode->nodeName;
+            if ($key === $itemKey) {
+                return $childNode;
+            }
+        }
+
+        return null;
+    }
+
+    protected function hasKey(?DOMElement $columnItemConfigurationArray, string $configKey): bool
+    {
+        if (! $columnItemConfigurationArray instanceof \DOMElement) {
+            return false;
+        }
+
+        foreach ($columnItemConfigurationArray->childNodes as $item) {
+            if (! $item instanceof DOMElement) {
+                continue;
+            }
+
+            if ($this->isValue($item->nodeName, $configKey)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private function isConfigType(?DOMElement $columnItemConfigurationArray, string $type): bool
+    {
+        if (! $columnItemConfigurationArray instanceof \DOMElement) {
+            return false;
+        }
+
+        return $this->hasKeyValuePair($columnItemConfigurationArray, 'type', $type);
+    }
+
+    private function hasRenderType(DOMElement $columnItemConfigurationArray): bool
+    {
+        $renderTypeItem = $this->extractDomElementByKey($columnItemConfigurationArray, 'renderType');
+        return $renderTypeItem !== null;
+    }
+
+    private function hasKeyValuePair(DOMElement $configValueArray, string $configKey, string $expectedValue): bool
+    {
+        foreach ($configValueArray->childNodes as $configItemValue) {
+            if (! $configItemValue instanceof DOMElement) {
+                continue;
+            }
+
+            if ($this->isValue($configItemValue->nodeName, $configKey)
+                && $this->isValue($configItemValue->textContent, $expectedValue)
+            ) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private function isValue(string $element, string $value): bool
+    {
+        return $element === $value;
+    }
+}
diff --git a/src/Rector/v12/v0/flexform/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector.php b/src/Rector/v12/v0/flexform/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector.php
new file mode 100644
index 00000000..e3d0cf1a
--- /dev/null
+++ b/src/Rector/v12/v0/flexform/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector.php
@@ -0,0 +1,154 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Ssch\TYPO3Rector\Rector\v12\v0\flexform;
+
+use DOMDocument;
+use DOMElement;
+use DOMNodeList;
+use DOMXPath;
+use Ssch\TYPO3Rector\Contract\FileProcessor\FlexForms\Rector\FlexFormRectorInterface;
+use Ssch\TYPO3Rector\Helper\ArrayUtility;
+use Ssch\TYPO3Rector\Helper\FlexFormHelperTrait;
+use Ssch\TYPO3Rector\Helper\StringUtility;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @changelog https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/12.0/Feature-97193-NewTCATypeNumber.html
+ * @see \Ssch\TYPO3Rector\Tests\Rector\v12\v0\flexform\MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector\MigrateEvalIntAndDouble2ToTypeNumberFlexFormRectorTest
+ */
+final class MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector implements FlexFormRectorInterface
+{
+    use FlexFormHelperTrait;
+
+    /**
+     * @var string
+     */
+    private const INT = 'int';
+
+    /**
+     * @var string
+     */
+    private const DOUBLE2 = 'double2';
+
+    private bool $domDocumentHasBeenChanged = false;
+
+    public function transform(DOMDocument $domDocument): bool
+    {
+        $xpath = new DOMXPath($domDocument);
+
+        /** @var DOMNodeList<DOMElement> $elements */
+        $elements = $xpath->query('//config');
+
+        if ($elements->count() === 0) {
+            return false;
+        }
+
+        foreach ($elements as $element) {
+            $this->refactorColumn($domDocument, $element);
+        }
+
+        return $this->domDocumentHasBeenChanged;
+    }
+
+    /**
+     * @codeCoverageIgnore
+     */
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition('Migrate eval int and double2 to type number', [new CodeSample(
+            <<<'CODE_SAMPLE'
+<int_field>
+    <label>int field</label>
+    <config>
+        <type>input</type>
+        <eval>int</eval>
+    </config>
+</int_field>
+<double2_field>
+    <label>double2 field</label>
+    <config>
+        <type>input</type>
+        <eval>double2</eval>
+    </config>
+</double2_field>
+CODE_SAMPLE
+            ,
+            <<<'CODE_SAMPLE'
+<int_field>
+    <label>int field</label>
+    <config>
+        <type>number</type>
+    </config>
+</int_field>
+<double2_field>
+    <label>double2 field</label>
+    <config>
+        <type>number</type>
+        <format>decimal</format>
+    </config>
+</double2_field>
+CODE_SAMPLE
+        )]);
+    }
+
+    private function refactorColumn(DOMDocument $domDocument, ?DOMElement $configElement): void
+    {
+        if (! $configElement instanceof DOMElement) {
+            return;
+        }
+
+        if (! $this->isConfigType($configElement, 'input') || $this->hasRenderType($configElement)) {
+            return;
+        }
+
+        if (! $this->hasKey($configElement, 'eval')) {
+            return;
+        }
+
+        $evalDomElement = $this->extractDomElementByKey($configElement, 'eval');
+
+        if (! $evalDomElement instanceof DOMElement) {
+            return;
+        }
+
+        $evalListValue = $evalDomElement->nodeValue;
+
+        if (! is_string($evalListValue)) {
+            return;
+        }
+
+        if (StringUtility::inList($evalListValue, self::INT) || StringUtility::inList($evalListValue, self::DOUBLE2)) {
+            $evalList = ArrayUtility::trimExplode(',', $evalListValue, true);
+
+            // Remove "int" from $evalList
+            $evalList = array_filter(
+                $evalList,
+                static fn (string $eval) => $eval !== self::INT && $eval !== self::DOUBLE2
+            );
+
+            if ($evalList !== []) {
+                // Write back filtered 'eval'
+                $evalDomElement->nodeValue = '';
+                $evalDomElement->appendChild($domDocument->createTextNode(implode(',', $evalList)));
+            } elseif ($evalDomElement->parentNode instanceof DOMElement) {
+                // 'eval' is empty, remove whole configuration
+                $evalDomElement->parentNode->removeChild($evalDomElement);
+            }
+        }
+
+        $toChangeItem = $this->extractDomElementByKey($configElement, 'type');
+        if ($toChangeItem instanceof DOMElement) {
+            $toChangeItem->nodeValue = '';
+            $toChangeItem->appendChild($domDocument->createTextNode('number'));
+        }
+
+        if (StringUtility::inList($evalListValue, self::DOUBLE2)) {
+            $configElement->appendChild($domDocument->createElement('format', 'decimal'));
+        }
+
+        $this->domDocumentHasBeenChanged = true;
+    }
+}
diff --git a/src/Rector/v12/v0/tca/MigrateEvalIntAndDouble2ToTypeNumberRector.php b/src/Rector/v12/v0/tca/MigrateEvalIntAndDouble2ToTypeNumberRector.php
new file mode 100644
index 00000000..2e9479ff
--- /dev/null
+++ b/src/Rector/v12/v0/tca/MigrateEvalIntAndDouble2ToTypeNumberRector.php
@@ -0,0 +1,135 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Ssch\TYPO3Rector\Rector\v12\v0\tca;
+
+use PhpParser\Node\Expr;
+use PhpParser\Node\Expr\Array_;
+use PhpParser\Node\Expr\ArrayItem;
+use PhpParser\Node\Scalar\String_;
+use Ssch\TYPO3Rector\Helper\ArrayUtility;
+use Ssch\TYPO3Rector\Helper\StringUtility;
+use Ssch\TYPO3Rector\Helper\TcaHelperTrait;
+use Ssch\TYPO3Rector\Rector\Tca\AbstractTcaRector;
+use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
+use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
+
+/**
+ * @changelog https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/12.0/Feature-97193-NewTCATypeNumber.html
+ * @see \Ssch\TYPO3Rector\Tests\Rector\v12\v0\tca\MigrateEvalIntAndDouble2ToTypeNumberRector\MigrateEvalIntAndDouble2ToTypeNumberRectorTest
+ */
+final class MigrateEvalIntAndDouble2ToTypeNumberRector extends AbstractTcaRector
+{
+    use TcaHelperTrait;
+
+    /**
+     * @var string
+     */
+    private const INT = 'int';
+
+    /**
+     * @var string
+     */
+    private const DOUBLE2 = 'double2';
+
+    /**
+     * @codeCoverageIgnore
+     */
+    public function getRuleDefinition(): RuleDefinition
+    {
+        return new RuleDefinition('Migrate eval int and double2 to type number', [new CodeSample(
+            <<<'CODE_SAMPLE'
+'int_field' => [
+    'label' => 'int field',
+    'config' => [
+        'type' => 'input',
+        'eval' => 'int',
+    ],
+],
+'double2_field' => [
+    'label' => 'double2 field',
+    'config' => [
+        'type' => 'input',
+        'eval' => 'double2',
+    ],
+],
+CODE_SAMPLE
+            ,
+            <<<'CODE_SAMPLE'
+'int_field' => [
+    'label' => 'int field',
+    'config' => [
+        'type' => 'number',
+    ],
+],
+'double2_field' => [
+    'label' => 'double2 field',
+    'config' => [
+        'type' => 'number',
+        'format' => 'decimal',
+    ],
+],
+CODE_SAMPLE
+        )]);
+    }
+
+    protected function refactorColumn(Expr $columnName, Expr $columnTca): void
+    {
+        $configArray = $this->extractSubArrayByKey($columnTca, self::CONFIG);
+        if (! $configArray instanceof Array_) {
+            return;
+        }
+
+        // Return early, if not TCA type "input" or a renderType is set
+        // or neither eval=int nor eval=double2 are set.
+        if (! $this->isConfigType($configArray, 'input') || $this->hasRenderType($configArray)) {
+            return;
+        }
+
+        if (! $this->hasKey($configArray, 'eval')) {
+            return;
+        }
+
+        $evalArrayItem = $this->extractArrayItemByKey($configArray, 'eval');
+
+        if (! $evalArrayItem instanceof ArrayItem) {
+            return;
+        }
+
+        $evalListValue = $this->valueResolver->getValue($evalArrayItem->value);
+
+        if (! is_string($evalListValue)) {
+            return;
+        }
+
+        if (StringUtility::inList($evalListValue, self::INT) || StringUtility::inList($evalListValue, self::DOUBLE2)) {
+            $evalList = ArrayUtility::trimExplode(',', $evalListValue, true);
+
+            // Remove "int" from $evalList
+            $evalList = array_filter(
+                $evalList,
+                static fn (string $eval) => $eval !== self::INT && $eval !== self::DOUBLE2
+            );
+
+            if ($evalList !== []) {
+                // Write back filtered 'eval'
+                $evalArrayItem->value = new String_(implode(',', $evalList));
+            } else {
+                // 'eval' is empty, remove whole configuration
+                $this->removeNode($evalArrayItem);
+            }
+        }
+
+        $toChangeArrayItem = $this->extractArrayItemByKey($configArray, 'type');
+        if ($toChangeArrayItem instanceof ArrayItem) {
+            $toChangeArrayItem->value = new String_('number');
+        }
+
+        if (StringUtility::inList($evalListValue, self::DOUBLE2)) {
+            $configArray->items[] = new ArrayItem(new String_('decimal'), new String_('format'));
+        }
+
+        $this->hasAstBeenChanged = true;
+    }
+}


Test patch:
diff --git a/tests/Rector/v12/v0/flexform/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector/Fixture/fixture.xml.inc b/tests/Rector/v12/v0/flexform/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector/Fixture/fixture.xml.inc
new file mode 100644
index 00000000..c43df827
--- /dev/null
+++ b/tests/Rector/v12/v0/flexform/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector/Fixture/fixture.xml.inc
@@ -0,0 +1,65 @@
+<T3DataStructure>
+    <sheets>
+        <sDEF>
+            <ROOT>
+                <sheetTitle>sheetTitle</sheetTitle>
+                <type>array</type>
+                <el>
+                    <settings.int_field>
+                        <label>Int field</label>
+                        <config>
+                            <type>input</type>
+                            <eval>int</eval>
+                        </config>
+                    </settings.int_field>
+                    <settings.nullable_int_field>
+                        <label>Nullable int field</label>
+                        <config>
+                            <type>input</type>
+                            <eval>int,null</eval>
+                        </config>
+                    </settings.nullable_int_field>
+                    <settings.double2_field>
+                        <label>double2 field</label>
+                        <config>
+                            <type>input</type>
+                            <eval>double2</eval>
+                        </config>
+                    </settings.double2_field>
+                </el>
+            </ROOT>
+        </sDEF>
+    </sheets>
+</T3DataStructure>
+-----
+<T3DataStructure>
+    <sheets>
+        <sDEF>
+            <ROOT>
+                <sheetTitle>sheetTitle</sheetTitle>
+                <type>array</type>
+                <el>
+                    <settings.int_field>
+                        <label>Int field</label>
+                        <config>
+                            <type>number</type>
+                        </config>
+                    </settings.int_field>
+                    <settings.nullable_int_field>
+                        <label>Nullable int field</label>
+                        <config>
+                            <type>number</type>
+                            <eval>null</eval>
+                        </config>
+                    </settings.nullable_int_field>
+                    <settings.double2_field>
+                        <label>double2 field</label>
+                        <config>
+                            <type>number</type>
+                        <format>decimal</format></config>
+                    </settings.double2_field>
+                </el>
+            </ROOT>
+        </sDEF>
+    </sheets>
+</T3DataStructure>
diff --git a/tests/Rector/v12/v0/flexform/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRectorTest.php b/tests/Rector/v12/v0/flexform/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRectorTest.php
new file mode 100644
index 00000000..6384c101
--- /dev/null
+++ b/tests/Rector/v12/v0/flexform/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRectorTest.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Ssch\TYPO3Rector\Tests\Rector\v12\v0\flexform\MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector;
+
+use Iterator;
+use Rector\Testing\PHPUnit\AbstractRectorTestCase;
+
+final class MigrateEvalIntAndDouble2ToTypeNumberFlexFormRectorTest extends AbstractRectorTestCase
+{
+    /**
+     * @dataProvider provideData()
+     */
+    public function test(string $filePath): void
+    {
+        $this->doTestFile($filePath);
+    }
+
+    /**
+     * @return Iterator<array<string>>
+     */
+    public function provideData(): Iterator
+    {
+        return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture', '*.xml.inc');
+    }
+
+    public function provideConfigFilePath(): string
+    {
+        return __DIR__ . '/config/configured_rule.php';
+    }
+}
diff --git a/tests/Rector/v12/v0/flexform/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector/config/configured_rule.php b/tests/Rector/v12/v0/flexform/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector/config/configured_rule.php
new file mode 100644
index 00000000..0b62a31d
--- /dev/null
+++ b/tests/Rector/v12/v0/flexform/MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector/config/configured_rule.php
@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+use Rector\Config\RectorConfig;
+use Ssch\TYPO3Rector\Rector\v12\v0\flexform\MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector;
+
+return static function (RectorConfig $rectorConfig): void {
+    $rectorConfig->import(__DIR__ . '/../../../../../../../config/config_test.php');
+    $rectorConfig->rule(MigrateEvalIntAndDouble2ToTypeNumberFlexFormRector::class);
+};
diff --git a/tests/Rector/v12/v0/tca/MigrateEvalIntAndDouble2ToTypeNumberRector/Fixture/fixture.php.inc b/tests/Rector/v12/v0/tca/MigrateEvalIntAndDouble2ToTypeNumberRector/Fixture/fixture.php.inc
new file mode 100644
index 00000000..06c79741
--- /dev/null
+++ b/tests/Rector/v12/v0/tca/MigrateEvalIntAndDouble2ToTypeNumberRector/Fixture/fixture.php.inc
@@ -0,0 +1,90 @@
+<?php
+
+namespace Ssch\TYPO3Rector\Tests\Rector\v12\v0\tca\MigrateEvalIntAndDouble2ToTypeNumberRector\Fixture;
+
+return [
+    'ctrl' => [],
+    'columns' => [
+        'int_field' => [
+            'label' => 'int field',
+            'config' => [
+                'type' => 'input',
+                'eval' => 'int',
+            ],
+        ],
+        'nullable_int_field' => [
+            'label' => 'Nullable int field',
+            'config' => [
+                'type' => 'input',
+                'eval' => 'int,null',
+            ],
+        ],
+        'double2_field' => [
+            'label' => 'double2 field',
+            'config' => [
+                'type' => 'input',
+                'eval' => 'double2',
+            ],
+        ],
+        'already_number_field' => [
+            'label' => 'int field',
+            'config' => [
+                'type' => 'number'
+            ],
+        ],
+        'already_decimal_number_field' => [
+            'label' => 'double2 field',
+            'config' => [
+                'type' => 'number',
+                'format' => 'decimal'
+            ],
+        ],
+    ],
+];
+
+?>
+-----
+<?php
+
+namespace Ssch\TYPO3Rector\Tests\Rector\v12\v0\tca\MigrateEvalIntAndDouble2ToTypeNumberRector\Fixture;
+
+return [
+    'ctrl' => [],
+    'columns' => [
+        'int_field' => [
+            'label' => 'int field',
+            'config' => [
+                'type' => 'number',
+            ],
+        ],
+        'nullable_int_field' => [
+            'label' => 'Nullable int field',
+            'config' => [
+                'type' => 'number',
+                'eval' => 'null',
+            ],
+        ],
+        'double2_field' => [
+            'label' => 'double2 field',
+            'config' => [
+                'type' => 'number',
+                'format' => 'decimal',
+            ],
+        ],
+        'already_number_field' => [
+            'label' => 'int field',
+            'config' => [
+                'type' => 'number'
+            ],
+        ],
+        'already_decimal_number_field' => [
+            'label' => 'double2 field',
+            'config' => [
+                'type' => 'number',
+                'format' => 'decimal'
+            ],
+        ],
+    ],
+];
+
+?>
diff --git a/tests/Rector/v12/v0/tca/MigrateEvalIntAndDouble2ToTypeNumberRector/MigrateEvalIntAndDouble2ToTypeNumberRectorTest.php b/tests/Rector/v12/v0/tca/MigrateEvalIntAndDouble2ToTypeNumberRector/MigrateEvalIntAndDouble2ToTypeNumberRectorTest.php
new file mode 100644
index 00000000..990bdf94
--- /dev/null
+++ b/tests/Rector/v12/v0/tca/MigrateEvalIntAndDouble2ToTypeNumberRector/MigrateEvalIntAndDouble2ToTypeNumberRectorTest.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Ssch\TYPO3Rector\Tests\Rector\v12\v0\tca\MigrateEvalIntAndDouble2ToTypeNumberRector;
+
+use Iterator;
+use Rector\Testing\PHPUnit\AbstractRectorTestCase;
+
+final class MigrateEvalIntAndDouble2ToTypeNumberRectorTest extends AbstractRectorTestCase
+{
+    /**
+     * @dataProvider provideData()
+     */
+    public function test(string $filePath): void
+    {
+        $this->doTestFile($filePath);
+    }
+
+    /**
+     * @return Iterator<array<string>>
+     */
+    public function provideData(): Iterator
+    {
+        return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
+    }
+
+    public function provideConfigFilePath(): string
+    {
+        return __DIR__ . '/config/configured_rule.php';
+    }
+}
diff --git a/tests/Rector/v12/v0/tca/MigrateEvalIntAndDouble2ToTypeNumberRector/config/configured_rule.php b/tests/Rector/v12/v0/tca/MigrateEvalIntAndDouble2ToTypeNumberRector/config/configured_rule.php
new file mode 100644
index 00000000..9f10dc56
--- /dev/null
+++ b/tests/Rector/v12/v0/tca/MigrateEvalIntAndDouble2ToTypeNumberRector/config/configured_rule.php
@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+use Rector\Config\RectorConfig;
+use Ssch\TYPO3Rector\Rector\v12\v0\tca\MigrateEvalIntAndDouble2ToTypeNumberRector;
+
+return static function (RectorConfig $rectorConfig): void {
+    $rectorConfig->import(__DIR__ . '/../../../../../../../config/config_test.php');
+    $rectorConfig->rule(MigrateEvalIntAndDouble2ToTypeNumberRector::class);
+};