diff --git a/.gitattributes b/.gitattributes index 44227db6362..c0e4f848835 100644 --- a/.gitattributes +++ b/.gitattributes @@ -41,3 +41,4 @@ phpstan-for-rector.neon export-ignore # testing Windows spaces - https://help.github.com/en/github/using-git/configuring-git-to-handle-line-endings packages/better-php-doc-parser/tests/PhpDocInfo/PhpDocInfoPrinter/FixtureBasic/with_spac.txt text eol=crlf +packages-tests/FileFormatter/ValueObject/Fixture/composer_carriage_return_line_feed.json json eol=crlf diff --git a/composer.json b/composer.json index 41498620cf7..eed6a2c111f 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,8 @@ "composer/xdebug-handler": "^2.0", "danielstjules/stringy": "^3.1", "doctrine/inflector": "^2.0", + "ergebnis/json-printer": "^3.1", + "idiosyncratic/editorconfig": "^0.1.0", "jean85/pretty-package-versions": "^1.6", "nette/caching": "^3.1", "nette/utils": "^3.2", @@ -48,6 +50,7 @@ "rector/rector-phpunit": "^0.10.8", "rector/rector-symfony": "^0.10.5", "sebastian/diff": "^4.0.4", + "shanethehat/pretty-xml": "^1.0", "symfony/console": "^4.4.8|^5.1", "symfony/dependency-injection": "^5.1", "symfony/finder": "^4.4.8|^5.1", @@ -153,7 +156,5 @@ "config": { "sort-packages": true, "platform-check": false - }, - "minimum-stability": "dev", - "prefer-stable": true + } } diff --git a/config/services.php b/config/services.php index 048ae9c511b..5b68545fb9c 100644 --- a/config/services.php +++ b/config/services.php @@ -5,6 +5,9 @@ declare(strict_types=1); use Composer\Semver\VersionParser; use Doctrine\Inflector\Inflector; use Doctrine\Inflector\Rules\English\InflectorFactory; +use Ergebnis\Json\Printer\Printer; +use Ergebnis\Json\Printer\PrinterInterface; +use Idiosyncratic\EditorConfig\EditorConfig; use Nette\Caching\Cache; use PhpParser\BuilderFactory; use PhpParser\Lexer; @@ -21,6 +24,7 @@ use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\Reflection\ReflectionProvider; +use PrettyXml\Formatter; use Rector\BetterPhpDocParser\PhpDocParser\BetterPhpDocParser; use Rector\BetterPhpDocParser\PhpDocParser\BetterTypeParser; use Rector\Caching\Cache\NetteCacheFactory; @@ -28,6 +32,8 @@ use Rector\Core\Console\ConsoleApplication; use Rector\Core\NonPhpFile\Rector\RenameClassNonPhpRector; use Rector\Core\PhpParser\Parser\NikicPhpParserFactory; use Rector\Core\PhpParser\Parser\PhpParserLexerFactory; +use Rector\FileFormatter\Contract\EditorConfig\EditorConfigParserInterface; +use Rector\FileFormatter\EditorConfig\EditorConfigIdiosyncraticParser; use Rector\NodeTypeResolver\DependencyInjection\PHPStanServicesFactory; use Rector\NodeTypeResolver\Reflection\BetterReflection\SourceLocator\IntermediateSourceLocator; use Rector\NodeTypeResolver\Reflection\BetterReflection\SourceLocatorProvider\DynamicSourceLocatorProvider; @@ -146,4 +152,13 @@ return static function (ContainerConfigurator $containerConfigurator): void { ->factory([service(PHPStanServicesFactory::class), 'createTypeNodeResolver']); $services->set(DynamicSourceLocatorProvider::class) ->factory([service(PHPStanServicesFactory::class), 'createDynamicSourceLocatorProvider']); + + $services->set(Printer::class); + $services->alias(PrinterInterface::class, Printer::class); + + $services->set(Formatter::class); + + $services->set(EditorConfig::class); + + $services->alias(EditorConfigParserInterface::class, EditorConfigIdiosyncraticParser::class); }; diff --git a/packages-tests/FileFormatter/EditorConfig/EditorConfigIdiosyncraticParser/.editorconfig b/packages-tests/FileFormatter/EditorConfig/EditorConfigIdiosyncraticParser/.editorconfig new file mode 100644 index 00000000000..d8a51d6e3bb --- /dev/null +++ b/packages-tests/FileFormatter/EditorConfig/EditorConfigIdiosyncraticParser/.editorconfig @@ -0,0 +1,3 @@ +[composer.json] +indent_size = 1 +indent_style = tab diff --git a/packages-tests/FileFormatter/EditorConfig/EditorConfigIdiosyncraticParser/EditorConfigIdiosyncraticParserTest.php b/packages-tests/FileFormatter/EditorConfig/EditorConfigIdiosyncraticParser/EditorConfigIdiosyncraticParserTest.php new file mode 100644 index 00000000000..cecf58ae4bf --- /dev/null +++ b/packages-tests/FileFormatter/EditorConfig/EditorConfigIdiosyncraticParser/EditorConfigIdiosyncraticParserTest.php @@ -0,0 +1,42 @@ +boot(); + $this->editorConfigParser = $this->getService(EditorConfigParserInterface::class); + } + + public function testComposerJsonFile(): void + { + $editorConfigConfigurationBuilder = EditorConfigConfigurationBuilder::anEditorConfigConfiguration(); + $editorConfigConfigurationBuilder->withIndent(Indent::createSpaceWithSize(20)); + + $composerJsonFile = new SmartFileInfo(__DIR__ . '/Fixture/composer.json'); + + $file = new File($composerJsonFile, $composerJsonFile->getContents()); + + $editorConfigConfiguration = $this->editorConfigParser->extractConfigurationForFile( + $file, + $editorConfigConfigurationBuilder + ); + + $this->assertSame('tab', $editorConfigConfiguration->getIndentStyle()); + $this->assertSame(1, $editorConfigConfiguration->getIndentSize()); + } +} diff --git a/packages-tests/FileFormatter/EditorConfig/EditorConfigIdiosyncraticParser/Fixture/composer.json b/packages-tests/FileFormatter/EditorConfig/EditorConfigIdiosyncraticParser/Fixture/composer.json new file mode 100644 index 00000000000..fe051e7e968 --- /dev/null +++ b/packages-tests/FileFormatter/EditorConfig/EditorConfigIdiosyncraticParser/Fixture/composer.json @@ -0,0 +1,5 @@ +{ + "name": "foo/bar", + "description": "A foo bar baz extension", + "license": "GPL-2.0-or-later" +} diff --git a/packages-tests/FileFormatter/Formatter/JsonFileFormatter/Fixture/composer_change_indent_style_to_tab.json b/packages-tests/FileFormatter/Formatter/JsonFileFormatter/Fixture/composer_change_indent_style_to_tab.json new file mode 100644 index 00000000000..07b8c998133 --- /dev/null +++ b/packages-tests/FileFormatter/Formatter/JsonFileFormatter/Fixture/composer_change_indent_style_to_tab.json @@ -0,0 +1,11 @@ +{ + "name": "foo/bar", + "description": "A foo bar baz extension", + "license": "GPL-2.0-or-later" +} +----- +{ + "name": "foo/bar", + "description": "A foo bar baz extension", + "license": "GPL-2.0-or-later" +} diff --git a/packages-tests/FileFormatter/Formatter/JsonFileFormatter/JsonFileFormatterTest.php b/packages-tests/FileFormatter/Formatter/JsonFileFormatter/JsonFileFormatterTest.php new file mode 100644 index 00000000000..5fbb470e997 --- /dev/null +++ b/packages-tests/FileFormatter/Formatter/JsonFileFormatter/JsonFileFormatterTest.php @@ -0,0 +1,60 @@ +boot(); + $this->jsonFileFormatter = $this->getService(JsonFileFormatter::class); + } + + /** + * @dataProvider provideData() + */ + public function test(SmartFileInfo $fileInfo): void + { + $this->doTestFileInfo($fileInfo); + } + + /** + * @return Iterator> + */ + public function provideData(): Iterator + { + return StaticFixtureFinder::yieldDirectory(__DIR__ . '/Fixture', '*.json'); + } + + private function doTestFileInfo(SmartFileInfo $smartFileInfo): void + { + $inputFileInfoAndExpected = StaticFixtureSplitter::splitFileInfoToLocalInputAndExpected($smartFileInfo); + + $inputFileInfo = $inputFileInfoAndExpected->getInputFileInfo(); + $file = new File($inputFileInfo, $inputFileInfo->getContents()); + + $editorConfigConfigurationBuilder = EditorConfigConfigurationBuilder::anEditorConfigConfiguration(); + $editorConfigConfigurationBuilder->withIndent(Indent::createTabWithSize(1)); + + $this->jsonFileFormatter->format($file, $editorConfigConfigurationBuilder->build()); + + $this->assertSame($inputFileInfoAndExpected->getExpected(), $file->getFileContent()); + } +} diff --git a/packages-tests/FileFormatter/Formatter/XmlFileFormatter/Fixture/change_from_spaces_to_tabs.xml b/packages-tests/FileFormatter/Formatter/XmlFileFormatter/Fixture/change_from_spaces_to_tabs.xml new file mode 100644 index 00000000000..548304f240d --- /dev/null +++ b/packages-tests/FileFormatter/Formatter/XmlFileFormatter/Fixture/change_from_spaces_to_tabs.xml @@ -0,0 +1,25 @@ + + + +Gambardella, Matthew + XML Developer's Guide + Computer + 44.95 + 2000-10-01 + An in-depth look at creating applications + with XML. + + +----- + + + + Gambardella, Matthew + XML Developer's Guide + Computer + 44.95 + 2000-10-01 + An in-depth look at creating applications + with XML. + + diff --git a/packages-tests/FileFormatter/Formatter/XmlFileFormatter/XmlFileFormatterTest.php b/packages-tests/FileFormatter/Formatter/XmlFileFormatter/XmlFileFormatterTest.php new file mode 100644 index 00000000000..cef2be40530 --- /dev/null +++ b/packages-tests/FileFormatter/Formatter/XmlFileFormatter/XmlFileFormatterTest.php @@ -0,0 +1,60 @@ +boot(); + $this->xmlFileFormatter = $this->getService(XmlFileFormatter::class); + } + + /** + * @dataProvider provideData() + */ + public function test(SmartFileInfo $fileInfo): void + { + $this->doTestFileInfo($fileInfo); + } + + /** + * @return Iterator> + */ + public function provideData(): Iterator + { + return StaticFixtureFinder::yieldDirectory(__DIR__ . '/Fixture', '*.xml'); + } + + private function doTestFileInfo(SmartFileInfo $smartFileInfo): void + { + $inputFileInfoAndExpected = StaticFixtureSplitter::splitFileInfoToLocalInputAndExpected($smartFileInfo); + + $inputFileInfo = $inputFileInfoAndExpected->getInputFileInfo(); + $file = new File($inputFileInfo, $inputFileInfo->getContents()); + + $editorConfigConfigurationBuilder = EditorConfigConfigurationBuilder::anEditorConfigConfiguration(); + $editorConfigConfigurationBuilder->withIndent(Indent::createTabWithSize(1)); + + $this->xmlFileFormatter->format($file, $editorConfigConfigurationBuilder->build()); + + $this->assertSame($inputFileInfoAndExpected->getExpected(), $file->getFileContent()); + } +} diff --git a/packages-tests/FileFormatter/Formatter/YamlFileFormatter/Fixture/yaml_change_indent_size_from_two_to_four.yaml b/packages-tests/FileFormatter/Formatter/YamlFileFormatter/Fixture/yaml_change_indent_size_from_two_to_four.yaml new file mode 100644 index 00000000000..aa5d6d4a791 --- /dev/null +++ b/packages-tests/FileFormatter/Formatter/YamlFileFormatter/Fixture/yaml_change_indent_size_from_two_to_four.yaml @@ -0,0 +1,15 @@ +martin: + name: Martin + job: Developer + skills: + - python + - perl + - pascal +----- +martin: + name: Martin + job: Developer + skills: + - python + - perl + - pascal diff --git a/packages-tests/FileFormatter/Formatter/YamlFileFormatter/YamlFileFormatterTest.php b/packages-tests/FileFormatter/Formatter/YamlFileFormatter/YamlFileFormatterTest.php new file mode 100644 index 00000000000..0c366519b1b --- /dev/null +++ b/packages-tests/FileFormatter/Formatter/YamlFileFormatter/YamlFileFormatterTest.php @@ -0,0 +1,61 @@ +boot(); + $this->yamlFileFormatter = $this->getService(YamlFileFormatter::class); + } + + /** + * @dataProvider provideData() + */ + public function test(SmartFileInfo $fileInfo): void + { + $this->doTestFileInfo($fileInfo); + } + + /** + * @return Iterator> + */ + public function provideData(): Iterator + { + return StaticFixtureFinder::yieldDirectory(__DIR__ . '/Fixture', '*.yaml'); + } + + private function doTestFileInfo(SmartFileInfo $smartFileInfo): void + { + $inputFileInfoAndExpected = StaticFixtureSplitter::splitFileInfoToLocalInputAndExpected($smartFileInfo); + + $inputFileInfo = $inputFileInfoAndExpected->getInputFileInfo(); + $file = new File($inputFileInfo, $inputFileInfo->getContents()); + + $editorConfigConfigurationBuilder = EditorConfigConfigurationBuilder::anEditorConfigConfiguration(); + $editorConfigConfigurationBuilder->withIndent(Indent::createSpaceWithSize(4)); + $editorConfigConfigurationBuilder->withInsertFinalNewline(false); + + $this->yamlFileFormatter->format($file, $editorConfigConfigurationBuilder->build()); + + $this->assertSame($inputFileInfoAndExpected->getExpected(), $file->getFileContent()); + } +} diff --git a/packages-tests/FileFormatter/ValueObject/EditorConfigConfigurationTest.php b/packages-tests/FileFormatter/ValueObject/EditorConfigConfigurationTest.php new file mode 100644 index 00000000000..00d75cd18d2 --- /dev/null +++ b/packages-tests/FileFormatter/ValueObject/EditorConfigConfigurationTest.php @@ -0,0 +1,49 @@ +build(); + + $this->assertSame(StaticEolConfiguration::getEolChar(), $editorConfigConfiguration->getFinalNewline()); + } + + public function testWithoutFinalNewline(): void + { + $editorConfigConfigurationBuilder = EditorConfigConfigurationBuilder::anEditorConfigConfiguration(); + $editorConfigConfigurationBuilder->withInsertFinalNewline(false); + + $editorConfigConfiguration = $editorConfigConfigurationBuilder->build(); + + $this->assertSame('', $editorConfigConfiguration->getFinalNewline()); + } + + public function testIndentForTab(): void + { + $editorConfigConfigurationBuilder = EditorConfigConfigurationBuilder::anEditorConfigConfiguration(); + $editorConfigConfigurationBuilder->withIndent(Indent::createTabWithSize(4)); + + $editorConfigConfiguration = $editorConfigConfigurationBuilder->build(); + + $this->assertSame(' ', $editorConfigConfiguration->getIndent()); + } + + public function testIndentForSpace(): void + { + $editorConfigConfigurationBuilder = EditorConfigConfigurationBuilder::anEditorConfigConfiguration(); + $editorConfigConfigurationBuilder->withIndent(Indent::createSpaceWithSize(10)); + + $editorConfigConfiguration = $editorConfigConfigurationBuilder->build(); + + $this->assertSame(' ', $editorConfigConfiguration->getIndent()); + } +} diff --git a/packages-tests/FileFormatter/ValueObject/Fixture/composer_carriage_return.json b/packages-tests/FileFormatter/ValueObject/Fixture/composer_carriage_return.json new file mode 100644 index 00000000000..e1357a8bc0d --- /dev/null +++ b/packages-tests/FileFormatter/ValueObject/Fixture/composer_carriage_return.json @@ -0,0 +1 @@ +{ "name": "foo/bar", "description": "A foo bar baz extension", "license": "GPL-2.0-or-later" } \ No newline at end of file diff --git a/packages-tests/FileFormatter/ValueObject/Fixture/composer_carriage_return_line_feed.json b/packages-tests/FileFormatter/ValueObject/Fixture/composer_carriage_return_line_feed.json new file mode 100644 index 00000000000..fe051e7e968 --- /dev/null +++ b/packages-tests/FileFormatter/ValueObject/Fixture/composer_carriage_return_line_feed.json @@ -0,0 +1,5 @@ +{ + "name": "foo/bar", + "description": "A foo bar baz extension", + "license": "GPL-2.0-or-later" +} diff --git a/packages-tests/FileFormatter/ValueObject/Fixture/composer_indentation_space_six.json b/packages-tests/FileFormatter/ValueObject/Fixture/composer_indentation_space_six.json new file mode 100644 index 00000000000..8bb73ebece1 --- /dev/null +++ b/packages-tests/FileFormatter/ValueObject/Fixture/composer_indentation_space_six.json @@ -0,0 +1,5 @@ +{ + "name": "foo/bar", + "description": "A foo bar baz extension", + "license": "GPL-2.0-or-later" +} diff --git a/packages-tests/FileFormatter/ValueObject/Fixture/composer_indentation_tab_two.json b/packages-tests/FileFormatter/ValueObject/Fixture/composer_indentation_tab_two.json new file mode 100644 index 00000000000..3f208c13a1c --- /dev/null +++ b/packages-tests/FileFormatter/ValueObject/Fixture/composer_indentation_tab_two.json @@ -0,0 +1,5 @@ +{ + "name": "foo/bar", + "description": "A foo bar baz extension", + "license": "GPL-2.0-or-later" +} diff --git a/packages-tests/FileFormatter/ValueObject/Fixture/composer_line_feed.json b/packages-tests/FileFormatter/ValueObject/Fixture/composer_line_feed.json new file mode 100644 index 00000000000..fe051e7e968 --- /dev/null +++ b/packages-tests/FileFormatter/ValueObject/Fixture/composer_line_feed.json @@ -0,0 +1,5 @@ +{ + "name": "foo/bar", + "description": "A foo bar baz extension", + "license": "GPL-2.0-or-later" +} diff --git a/packages-tests/FileFormatter/ValueObject/Fixture/xml_line_feed.xml b/packages-tests/FileFormatter/ValueObject/Fixture/xml_line_feed.xml new file mode 100644 index 00000000000..3f413a56a9b --- /dev/null +++ b/packages-tests/FileFormatter/ValueObject/Fixture/xml_line_feed.xml @@ -0,0 +1,12 @@ + + + +Gambardella, Matthew + XML Developer's Guide + Computer + 44.95 + 2000-10-01 + An in-depth look at creating applications + with XML. + + diff --git a/packages-tests/FileFormatter/ValueObject/Fixture/yaml_carriage_return.yaml b/packages-tests/FileFormatter/ValueObject/Fixture/yaml_carriage_return.yaml new file mode 100644 index 00000000000..690ff612785 --- /dev/null +++ b/packages-tests/FileFormatter/ValueObject/Fixture/yaml_carriage_return.yaml @@ -0,0 +1 @@ +martin: name: Martin job: Developer skills: - python - perl - pascal \ No newline at end of file diff --git a/packages-tests/FileFormatter/ValueObject/Fixture/yaml_indentation_space_four.yaml b/packages-tests/FileFormatter/ValueObject/Fixture/yaml_indentation_space_four.yaml new file mode 100644 index 00000000000..7b2a03d244f --- /dev/null +++ b/packages-tests/FileFormatter/ValueObject/Fixture/yaml_indentation_space_four.yaml @@ -0,0 +1,7 @@ +martin: + name: Martin + job: Developer + skills: + - python + - perl + - pascal diff --git a/packages-tests/FileFormatter/ValueObject/Fixture/yaml_indentation_space_two.yaml b/packages-tests/FileFormatter/ValueObject/Fixture/yaml_indentation_space_two.yaml new file mode 100644 index 00000000000..eb68623178f --- /dev/null +++ b/packages-tests/FileFormatter/ValueObject/Fixture/yaml_indentation_space_two.yaml @@ -0,0 +1,7 @@ +martin: + name: Martin + job: Developer + skills: + - python + - perl + - pascal diff --git a/packages-tests/FileFormatter/ValueObject/IndentTest.php b/packages-tests/FileFormatter/ValueObject/IndentTest.php new file mode 100644 index 00000000000..684fa8f7168 --- /dev/null +++ b/packages-tests/FileFormatter/ValueObject/IndentTest.php @@ -0,0 +1,104 @@ +getContents()); + $this->assertSame($expectedIndent, $indent->__toString()); + } + + /** + * @dataProvider provideSizeStyleAndIndentString + */ + public function testFromSizeAndStyle(int $size, string $style, string $string): void + { + $indent = Indent::fromSizeAndStyle($size, $style); + + $this->assertSame($string, $indent->__toString()); + $this->assertSame($size, $indent->getIndentSize()); + $this->assertSame($style, $indent->getIndentStyle()); + } + + public function testFromSizeAndStyleWithInvalidSizeThrowsException(): void + { + $this->expectException(InvalidIndentSizeException::class); + Indent::createTabWithSize(0); + } + + public function testFromSizeAndStyleWithInvalidStyleThrowsException(): void + { + $this->expectException(InvalidIndentStyleException::class); + Indent::fromSizeAndStyle(1, 'invalid'); + } + + public function testFromInvalidContentThrowsException(): void + { + $this->expectException(ParseIndentException::class); + Indent::fromContent('This is invalid content'); + } + + /** + * @return Generator> + */ + public function extractFromFiles(): Generator + { + yield 'Yaml file with space indentation of size 4' => [ + new SmartFileInfo(__DIR__ . '/Fixture/yaml_indentation_space_four.yaml'), + ' ', + ]; + + yield 'Yaml file with space indentation of size 2' => [ + new SmartFileInfo(__DIR__ . '/Fixture/yaml_indentation_space_two.yaml'), + ' ', + ]; + + yield 'Json file with tab indentation of size 2' => [ + new SmartFileInfo(__DIR__ . '/Fixture/composer_indentation_tab_two.json'), + ' ', + ]; + + yield 'Json file with space indentation of size 6' => [ + new SmartFileInfo(__DIR__ . '/Fixture/composer_indentation_space_six.json'), + ' ', + ]; + } + + /** + * @return Generator + */ + public function provideSizeStyleAndIndentString(): Generator + { + foreach ($this->sizes() as $size) { + foreach (Indent::CHARACTERS as $style => $character) { + $string = str_repeat($character, $size); + + yield [$size, $style, $string]; + } + } + } + + /** + * @return int[] + */ + private static function sizes(): array + { + return [ + 'int-one' => 1, + 'int-greater-than-one' => 5, + ]; + } +} diff --git a/packages-tests/FileFormatter/ValueObject/NewLineTest.php b/packages-tests/FileFormatter/ValueObject/NewLineTest.php new file mode 100644 index 00000000000..7a3eab88b9a --- /dev/null +++ b/packages-tests/FileFormatter/ValueObject/NewLineTest.php @@ -0,0 +1,110 @@ +getContents()); + $this->assertSame($expectedNewLine, $newLine->__toString()); + } + + /** + * @dataProvider provideInvalidNewLineString + */ + public function testFromStringRejectsInvalidNewLineString(string $string): void + { + $this->expectException(InvalidNewLineStringException::class); + + NewLine::fromSingleCharacter($string); + } + + /** + * @dataProvider provideValidNewLineString + */ + public function testFromStringReturnsNewLine(string $string): void + { + $newLine = NewLine::fromSingleCharacter($string); + + $this->assertSame($string, $newLine->__toString()); + } + + /** + * @dataProvider provideValidNewLineStringFromEditorConfig + */ + public function testFromEditorConfigReturnsNewLine(string $string, string $expected): void + { + $newLine = NewLine::fromEditorConfig($string); + + $this->assertSame($expected, $newLine->__toString()); + } + + /** + * @return Generator> + */ + public function provideValidNewLineString(): Generator + { + foreach (["\n", "\r", "\r\n"] as $string) { + yield [$string]; + } + } + + /** + * @return Generator> + */ + public function provideInvalidNewLineString(): Generator + { + foreach (["\t", " \r ", " \r\n ", " \n ", ' ', "\f", "\x0b", "\x85"] as $string) { + yield [$string]; + } + } + + /** + * @return Generator> + */ + public function extractFromFiles(): Generator + { + yield 'Yaml file with carriage return' => [ + new SmartFileInfo(__DIR__ . '/Fixture/yaml_carriage_return.yaml'), + "\r", + ]; + + yield 'Xml file with line feed' => [new SmartFileInfo(__DIR__ . '/Fixture/xml_line_feed.xml'), "\n"]; + + yield 'Json file with line feed' => [new SmartFileInfo(__DIR__ . '/Fixture/composer_line_feed.json'), "\n"]; + + yield 'Json file with carriage return' => [ + new SmartFileInfo(__DIR__ . '/Fixture/composer_carriage_return.json'), + "\r", + ]; + + yield 'Json file with carriage return and line feed' => [ + new SmartFileInfo(__DIR__ . '/Fixture/composer_carriage_return_line_feed.json'), + "\r\n", + ]; + } + + /** + * @return Generator> + */ + public function provideValidNewLineStringFromEditorConfig(): Generator + { + foreach ([ + 'lf' => "\n", + 'cr' => "\r", + 'crlf' => "\r\n", + ] as $editorConfig => $string) { + yield [$editorConfig, $string]; + } + } +} diff --git a/packages/FileFormatter/Contract/EditorConfig/EditorConfigParserInterface.php b/packages/FileFormatter/Contract/EditorConfig/EditorConfigParserInterface.php new file mode 100644 index 00000000000..6d6f383bd45 --- /dev/null +++ b/packages/FileFormatter/Contract/EditorConfig/EditorConfigParserInterface.php @@ -0,0 +1,40 @@ +editorConfig = $editorConfig; + } + + public function extractConfigurationForFile( + File $file, + EditorConfigConfigurationBuilder $editorConfigConfigurationBuilder + ): EditorConfigConfiguration { + $smartFileInfo = $file->getSmartFileInfo(); + $configuration = $this->editorConfig->getConfigForPath($smartFileInfo->getRealPath()); + + if (array_key_exists(self::INDENT_STYLE, $configuration)) { + $indentStyle = (string) $configuration[self::INDENT_STYLE]->getValue(); + + $editorConfigConfigurationBuilder->withIndentStyle($indentStyle); + } + + if (array_key_exists(self::INDENT_SIZE, $configuration)) { + $indentSize = (int) $configuration[self::INDENT_SIZE]->getValue(); + + $editorConfigConfigurationBuilder->withIndentSize($indentSize); + } + + if (array_key_exists(self::END_OF_LINE, $configuration)) { + $endOfLine = (string) $configuration[self::END_OF_LINE]->getValue(); + + $editorConfigConfigurationBuilder->withEndOfLineFromEditorConfig($endOfLine); + } + + if (array_key_exists(self::INSERT_FINAL_NEWLINE, $configuration)) { + $insertFinalNewline = (bool) $configuration[self::INSERT_FINAL_NEWLINE]->getValue(); + + $editorConfigConfigurationBuilder->withInsertFinalNewline($insertFinalNewline); + } + + if (array_key_exists(self::TAB_WIDTH, $configuration)) { + $editorConfigConfigurationBuilder->withIndentSize($configuration[self::TAB_WIDTH]->getValue()); + } + + return $editorConfigConfigurationBuilder->build(); + } +} diff --git a/packages/FileFormatter/Exception/InvalidIndentSizeException.php b/packages/FileFormatter/Exception/InvalidIndentSizeException.php new file mode 100644 index 00000000000..d99e9d4d3a1 --- /dev/null +++ b/packages/FileFormatter/Exception/InvalidIndentSizeException.php @@ -0,0 +1,17 @@ + $allowedStyles + */ + public static function fromStyleAndAllowedStyles(string $style, array $allowedStyles): self + { + $message = sprintf('Given style "%s" is not allowed. Allowed are "%s"', $style, implode(' ', $allowedStyles)); + + return new self($message); + } +} diff --git a/packages/FileFormatter/Exception/InvalidNewLineStringException.php b/packages/FileFormatter/Exception/InvalidNewLineStringException.php new file mode 100644 index 00000000000..9936e7645f4 --- /dev/null +++ b/packages/FileFormatter/Exception/InvalidNewLineStringException.php @@ -0,0 +1,20 @@ +editorConfigParser = $editorConfigParser; + $this->fileFormatters = $fileFormatters; + $this->parameterProvider = $parameterProvider; + } + + /** + * @param File[] $files + */ + public function format(array $files): void + { + foreach ($files as $file) { + if (! $file->hasChanged()) { + continue; + } + + foreach ($this->fileFormatters as $fileFormatter) { + if (! $fileFormatter->supports($file)) { + continue; + } + + $editorConfigConfigurationBuilder = $fileFormatter->createDefaultEditorConfigConfigurationBuilder(); + + $this->sniffOriginalFileContent($file, $editorConfigConfigurationBuilder); + + $editorConfiguration = $this->createEditorConfiguration($file, $editorConfigConfigurationBuilder); + + $fileFormatter->format($file, $editorConfiguration); + } + } + } + + private function sniffOriginalFileContent( + File $file, + EditorConfigConfigurationBuilder $editorConfigConfigurationBuilder + ): void { + // Try to sniff into the original content to get the indentation and new line + try { + $indent = Indent::fromContent($file->getOriginalFileContent()); + $editorConfigConfigurationBuilder->withIndent($indent); + } catch (ParseIndentException $parseIndentException) { + } + + try { + $newLine = NewLine::fromContent($file->getOriginalFileContent()); + $editorConfigConfigurationBuilder->withNewLine($newLine); + } catch (InvalidNewLineStringException $invalidNewLineStringException) { + } + } + + private function createEditorConfiguration( + File $file, + EditorConfigConfigurationBuilder $editorConfigConfigurationBuilder + ): EditorConfigConfiguration { + if (! $this->parameterProvider->provideBoolParameter(Option::ENABLE_EDITORCONFIG)) { + return $editorConfigConfigurationBuilder->build(); + } + + return $this->editorConfigParser->extractConfigurationForFile($file, $editorConfigConfigurationBuilder); + } +} diff --git a/packages/FileFormatter/Formatter/JsonFileFormatter.php b/packages/FileFormatter/Formatter/JsonFileFormatter.php new file mode 100644 index 00000000000..9bec24eed04 --- /dev/null +++ b/packages/FileFormatter/Formatter/JsonFileFormatter.php @@ -0,0 +1,57 @@ +jsonPrinter = $jsonPrinter; + } + + public function supports(File $file): bool + { + $smartFileInfo = $file->getSmartFileInfo(); + + return $smartFileInfo->getExtension() === 'json'; + } + + public function format(File $file, EditorConfigConfiguration $editorConfigConfiguration): void + { + $newFileContent = $this->jsonPrinter->print( + $file->getFileContent(), + $editorConfigConfiguration->getIndent(), + $editorConfigConfiguration->getNewLine() + ); + + $newFileContent .= $editorConfigConfiguration->getFinalNewline(); + + $file->changeFileContent($newFileContent); + } + + public function createDefaultEditorConfigConfigurationBuilder(): EditorConfigConfigurationBuilder + { + $editorConfigConfigurationBuilder = EditorConfigConfigurationBuilder::anEditorConfigConfiguration(); + + $editorConfigConfigurationBuilder->withIndent(Indent::createSpaceWithSize(4)); + + return $editorConfigConfigurationBuilder; + } +} diff --git a/packages/FileFormatter/Formatter/XmlFileFormatter.php b/packages/FileFormatter/Formatter/XmlFileFormatter.php new file mode 100644 index 00000000000..272ef825956 --- /dev/null +++ b/packages/FileFormatter/Formatter/XmlFileFormatter.php @@ -0,0 +1,56 @@ +xmlFormatter = $xmlFormatter; + } + + public function supports(File $file): bool + { + $smartFileInfo = $file->getSmartFileInfo(); + + return $smartFileInfo->getExtension() === 'xml'; + } + + public function format(File $file, EditorConfigConfiguration $editorConfigConfiguration): void + { + $this->xmlFormatter->setIndentCharacter($editorConfigConfiguration->getIndentStyleCharacter()); + $this->xmlFormatter->setIndentSize($editorConfigConfiguration->getIndentSize()); + + $newFileContent = $this->xmlFormatter->format($file->getFileContent()); + + $newFileContent .= $editorConfigConfiguration->getFinalNewline(); + + $file->changeFileContent($newFileContent); + } + + public function createDefaultEditorConfigConfigurationBuilder(): EditorConfigConfigurationBuilder + { + $editorConfigConfigurationBuilder = EditorConfigConfigurationBuilder::anEditorConfigConfiguration(); + + $editorConfigConfigurationBuilder->withIndent(Indent::createTabWithSize(1)); + + return $editorConfigConfigurationBuilder; + } +} diff --git a/packages/FileFormatter/Formatter/YamlFileFormatter.php b/packages/FileFormatter/Formatter/YamlFileFormatter.php new file mode 100644 index 00000000000..7787d5960c3 --- /dev/null +++ b/packages/FileFormatter/Formatter/YamlFileFormatter.php @@ -0,0 +1,45 @@ +getSmartFileInfo(); + + return in_array($smartFileInfo->getExtension(), ['yaml', 'yml'], true); + } + + public function format(File $file, EditorConfigConfiguration $editorConfigConfiguration): void + { + $yaml = Yaml::parse($file->getFileContent()); + + $newFileContent = Yaml::dump($yaml, 99, $editorConfigConfiguration->getIndentSize()); + + $newFileContent .= $editorConfigConfiguration->getFinalNewline(); + + $file->changeFileContent($newFileContent); + } + + public function createDefaultEditorConfigConfigurationBuilder(): EditorConfigConfigurationBuilder + { + $editorConfigConfigurationBuilder = EditorConfigConfigurationBuilder::anEditorConfigConfiguration(); + + $editorConfigConfigurationBuilder->withIndent(Indent::createSpaceWithSize(2)); + + return $editorConfigConfigurationBuilder; + } +} diff --git a/packages/FileFormatter/ValueObject/EditorConfigConfiguration.php b/packages/FileFormatter/ValueObject/EditorConfigConfiguration.php new file mode 100644 index 00000000000..1400a713960 --- /dev/null +++ b/packages/FileFormatter/ValueObject/EditorConfigConfiguration.php @@ -0,0 +1,63 @@ +indent = $indent; + $this->newLine = $newLine; + $this->insertFinalNewline = $insertFinalNewline; + } + + public function getNewLine(): string + { + return $this->newLine->__toString(); + } + + public function getFinalNewline(): string + { + return $this->insertFinalNewline ? $this->getNewLine() : ''; + } + + public function getIndent(): string + { + return $this->indent->__toString(); + } + + public function getIndentStyleCharacter(): string + { + return $this->indent->getIndentStyleCharacter(); + } + + public function getIndentStyle(): string + { + return $this->indent->getIndentStyle(); + } + + public function getIndentSize(): int + { + return $this->indent->getIndentSize(); + } +} diff --git a/packages/FileFormatter/ValueObject/Indent.php b/packages/FileFormatter/ValueObject/Indent.php new file mode 100644 index 00000000000..42ec1b6fa29 --- /dev/null +++ b/packages/FileFormatter/ValueObject/Indent.php @@ -0,0 +1,133 @@ + ' ', + self::TAB => "\t", + ]; + + /** + * @var string + */ + private const SPACE = 'space'; + + /** + * @var string + */ + private const TAB = 'tab'; + + /** + * @see https://regex101.com/r/A2XiaF/1 + * @var string + */ + private const VALID_INDENT_REGEX = '/^( *|\t+)$/'; + + /** + * @var int + */ + private const MINIMUM_SIZE = 1; + + /** + * @see https://regex101.com/r/3HFEjX/1 + * @var string + */ + private const PARSE_INDENT_REGEX = '/^(?P( +|\t+)).*/m'; + + /** + * @var string + */ + private $string; + + private function __construct(string $string) + { + $this->string = $string; + } + + public function __toString(): string + { + return $this->string; + } + + public static function fromString(string $string): self + { + $validIndent = preg_match(self::VALID_INDENT_REGEX, $string); + + if ($validIndent !== 1) { + throw InvalidIndentStringException::fromString($string); + } + + return new self($string); + } + + public static function createSpaceWithSize(int $size): self + { + return self::fromSizeAndStyle($size, self::SPACE); + } + + public static function createTabWithSize(int $size): self + { + return self::fromSizeAndStyle($size, self::TAB); + } + + public static function fromSizeAndStyle(int $size, string $style): self + { + if ($size < self::MINIMUM_SIZE) { + throw InvalidIndentSizeException::fromSizeAndMinimumSize($size, self::MINIMUM_SIZE); + } + + if (! array_key_exists($style, self::CHARACTERS)) { + throw InvalidIndentStyleException::fromStyleAndAllowedStyles($style, array_keys(self::CHARACTERS)); + } + + $value = str_repeat(self::CHARACTERS[$style], $size); + + return new self($value); + } + + public static function fromContent(string $string): self + { + $validIndent = preg_match(self::PARSE_INDENT_REGEX, $string, $match); + if ($validIndent === 1) { + return self::fromString($match['indent']); + } + + throw ParseIndentException::fromString($string); + } + + public function getIndentSize(): int + { + return strlen($this->string); + } + + public function getIndentStyle(): string + { + return $this->startsWithSpace() ? self::SPACE : self::TAB; + } + + public function getIndentStyleCharacter(): string + { + return $this->startsWithSpace() ? self::CHARACTERS[self::SPACE] : self::CHARACTERS[self::TAB]; + } + + private function startsWithSpace(): bool + { + return Strings::startsWith($this->string, ' '); + } +} diff --git a/packages/FileFormatter/ValueObject/NewLine.php b/packages/FileFormatter/ValueObject/NewLine.php new file mode 100644 index 00000000000..25c1efe5f6c --- /dev/null +++ b/packages/FileFormatter/ValueObject/NewLine.php @@ -0,0 +1,91 @@ + + */ + private const ALLOWED_END_OF_LINE = [ + self::LINE_FEED => "\n", + self::CARRIAGE_RETURN => "\r", + self::CARRIAGE_RETURN_LINE_FEED => "\r\n", + ]; + + /** + * @var string + */ + private $string; + + private function __construct(string $string) + { + $this->string = $string; + } + + public function __toString(): string + { + return $this->string; + } + + public static function fromSingleCharacter(string $string): self + { + $validNewLineRegularExpression = '/^(?>\r\n|\n|\r)$/'; + $validNewLine = preg_match($validNewLineRegularExpression, $string); + + if ($validNewLine !== 1) { + throw InvalidNewLineStringException::fromString($string); + } + + return new self($string); + } + + public static function fromContent(string $string): self + { + $validNewLineRegularExpression = '/(?P\r\n|\n|\r)/'; + $validNewLine = preg_match($validNewLineRegularExpression, $string, $match); + if ($validNewLine === 1) { + return self::fromSingleCharacter($match['newLine']); + } + + return self::fromSingleCharacter(PHP_EOL); + } + + public static function fromEditorConfig(string $endOfLine): self + { + if (! array_key_exists($endOfLine, self::ALLOWED_END_OF_LINE)) { + $allowedEndOfLineValues = array_keys(self::ALLOWED_END_OF_LINE); + $message = sprintf( + 'The endOfLine "%s" is not allowed. Allowed are "%s"', + $endOfLine, + implode(',', $allowedEndOfLineValues) + ); + throw InvalidNewLineStringException::create($message); + } + + return self::fromSingleCharacter(self::ALLOWED_END_OF_LINE[$endOfLine]); + } +} diff --git a/packages/FileFormatter/ValueObjectFactory/EditorConfigConfigurationBuilder.php b/packages/FileFormatter/ValueObjectFactory/EditorConfigConfigurationBuilder.php new file mode 100644 index 00000000000..d1c75195f0b --- /dev/null +++ b/packages/FileFormatter/ValueObjectFactory/EditorConfigConfigurationBuilder.php @@ -0,0 +1,99 @@ +indentStyle = 'space'; + $this->indentSize = 2; + $this->newLine = NewLine::fromEditorConfig('lf'); + $this->insertFinalNewline = true; + } + + public static function anEditorConfigConfiguration(): self + { + return new self(); + } + + public function withNewLine(NewLine $newLine): self + { + $this->newLine = $newLine; + + return $this; + } + + public function withIndent(Indent $indent): self + { + $this->indentSize = $indent->getIndentSize(); + $this->indentStyle = $indent->getIndentStyle(); + + return $this; + } + + public function withIndentStyle(string $indentStyle): self + { + $this->indentStyle = $indentStyle; + + return $this; + } + + public function withIndentSize(int $indentSize): self + { + $this->indentSize = $indentSize; + + return $this; + } + + public function withInsertFinalNewline(bool $insertFinalNewline): self + { + $this->insertFinalNewline = $insertFinalNewline; + + return $this; + } + + public function withEndOfLineFromEditorConfig(string $endOfLine): self + { + $this->newLine = NewLine::fromEditorConfig($endOfLine); + + return $this; + } + + public function build(): EditorConfigConfiguration + { + $newLine = $this->newLine; + + return new EditorConfigConfiguration( + Indent::fromSizeAndStyle($this->indentSize, $this->indentStyle), + $newLine, + $this->insertFinalNewline + ); + } +} diff --git a/src/Application/ApplicationFileProcessor.php b/src/Application/ApplicationFileProcessor.php index 665cd6ca58e..3a22e53f7c9 100644 --- a/src/Application/ApplicationFileProcessor.php +++ b/src/Application/ApplicationFileProcessor.php @@ -8,6 +8,7 @@ use Rector\Core\Application\FileDecorator\FileDiffFileDecorator; use Rector\Core\Configuration\Configuration; use Rector\Core\Contract\Processor\FileProcessorInterface; use Rector\Core\ValueObject\Application\File; +use Rector\FileFormatter\FileFormatter; use Symplify\SmartFileSystem\SmartFileSystem; final class ApplicationFileProcessor @@ -32,6 +33,11 @@ final class ApplicationFileProcessor */ private $fileDiffFileDecorator; + /** + * @var FileFormatter + */ + private $fileFormatter; + /** * @param FileProcessorInterface[] $fileProcessors */ @@ -39,12 +45,14 @@ final class ApplicationFileProcessor Configuration $configuration, SmartFileSystem $smartFileSystem, FileDiffFileDecorator $fileDiffFileDecorator, + FileFormatter $fileFormatter, array $fileProcessors = [] ) { $this->fileProcessors = $fileProcessors; $this->smartFileSystem = $smartFileSystem; $this->configuration = $configuration; $this->fileDiffFileDecorator = $fileDiffFileDecorator; + $this->fileFormatter = $fileFormatter; } /** @@ -54,6 +62,8 @@ final class ApplicationFileProcessor { $this->processFiles($files); + $this->fileFormatter->format($files); + $this->fileDiffFileDecorator->decorate($files); $this->printFiles($files); diff --git a/src/Configuration/Option.php b/src/Configuration/Option.php index ba5e7d1807e..2692771a643 100644 --- a/src/Configuration/Option.php +++ b/src/Configuration/Option.php @@ -162,4 +162,9 @@ final class Option * @var string */ public const TEMPLATE_TYPE = 'template-type'; + + /** + * @var string + */ + public const ENABLE_EDITORCONFIG = 'enable_editorconfig'; } diff --git a/src/Contract/Formatter/FileFormatterInterface.php b/src/Contract/Formatter/FileFormatterInterface.php new file mode 100644 index 00000000000..a291b4381e2 --- /dev/null +++ b/src/Contract/Formatter/FileFormatterInterface.php @@ -0,0 +1,16 @@ +