From e75c1592fc8b246207a32804afe6af016eba2947 Mon Sep 17 00:00:00 2001 From: Michal Niewrzal Date: Mon, 10 Oct 2016 15:06:02 +0200 Subject: [PATCH] Use PHP_CodeSniffer as a formatter (#35) Also adds uriTopath util function --- composer.json | 3 +- fixtures/format.php | 10 +-- fixtures/format_expected.php | 6 +- src/Formatter.php | 89 ++++++++++++++++++++ src/PhpDocument.php | 12 +-- src/Protocol/TextEdit.php | 6 ++ src/utils.php | 28 +++++- tests/FormatterTest.php | 28 ++++++ tests/Server/TextDocument/FormattingTest.php | 25 ++++-- tests/Utils/FileUriTest.php | 61 +++++++++++--- 10 files changed, 232 insertions(+), 36 deletions(-) create mode 100644 src/Formatter.php create mode 100644 tests/FormatterTest.php diff --git a/composer.json b/composer.json index ee05937..2eef524 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,8 @@ "nikic/php-parser": "^3.0.0beta1", "phpdocumentor/reflection-docblock": "^3.0", "sabre/event": "^4.0", - "felixfbecker/advanced-json-rpc": "^1.2" + "felixfbecker/advanced-json-rpc": "^1.2", + "squizlabs/php_codesniffer" : "^2.7" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/fixtures/format.php b/fixtures/format.php index b45ebab..f300b67 100644 --- a/fixtures/format.php +++ b/fixtures/format.php @@ -1,13 +1,13 @@ initStandard(self::findConfiguration($path)); + $file = $cs->processFile(null, $content); + $fixed = $file->fixer->fixFile(); + if (!$fixed && $file->getErrorCount() > 0) { + throw new Exception('Unable to format file'); + } + + $new = $file->fixer->getContents(); + if ($content === $new) { + return []; + } + return [new TextEdit(new Range(new Position(0, 0), self::calculateEndPosition($content)), $new)]; + } + + /** + * Calculate position of last character. + * + * @param string $content document as string + * + * @return \LanguageServer\Protocol\Position + */ + private static function calculateEndPosition(string $content): Position + { + $lines = explode("\n", $content); + return new Position(count($lines) - 1, strlen(end($lines))); + } + + /** + * Search for PHP_CodeSniffer configuration file at given directory or its parents. + * If no configuration found then PSR2 standard is loaded by default. + * + * @param string $path path to file or directory + * @return string[] + */ + private static function findConfiguration(string $path) + { + if (is_dir($path)) { + $currentDir = $path; + } else { + $currentDir = dirname($path); + } + do { + $default = $currentDir . DIRECTORY_SEPARATOR . 'phpcs.xml'; + if (is_file($default)) { + return [$default]; + } + + $default = $currentDir . DIRECTORY_SEPARATOR . 'phpcs.xml.dist'; + if (is_file($default)) { + return [$default]; + } + + $lastDir = $currentDir; + $currentDir = dirname($currentDir); + } while ($currentDir !== '.' && $currentDir !== $lastDir); + + $standard = PHP_CodeSniffer::getConfigData('default_standard') ?? 'PSR2'; + return explode(',', $standard); + } + +} diff --git a/src/PhpDocument.php b/src/PhpDocument.php index 6ba2900..963e0c9 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -218,20 +218,16 @@ class PhpDocument } /** - * Returns this document as formatted text. + * Returns array of TextEdit changes to format this document. * - * @return string + * @return \LanguageServer\Protocol\TextEdit[] */ public function getFormattedText() { - if (empty($this->stmts)) { + if (empty($this->getContent())) { return []; } - $prettyPrinter = new PrettyPrinter(); - $edit = new TextEdit(); - $edit->range = new Range(new Position(0, 0), new Position(PHP_INT_MAX, PHP_INT_MAX)); - $edit->newText = $prettyPrinter->prettyPrintFile($this->stmts); - return [$edit]; + return Formatter::format($this->content, $this->uri); } /** diff --git a/src/Protocol/TextEdit.php b/src/Protocol/TextEdit.php index c13a3c5..8ca1d43 100644 --- a/src/Protocol/TextEdit.php +++ b/src/Protocol/TextEdit.php @@ -22,4 +22,10 @@ class TextEdit * @var string */ public $newText; + + public function __construct(Range $range = null, string $newText = null) + { + $this->range = $range; + $this->newText = $newText; + } } diff --git a/src/utils.php b/src/utils.php index 3086efd..3c058e2 100644 --- a/src/utils.php +++ b/src/utils.php @@ -3,6 +3,8 @@ declare(strict_types = 1); namespace LanguageServer; +use InvalidArgumentException; + /** * Recursively Searches files with matching filename, starting at $path. * @@ -29,6 +31,30 @@ function findFilesRecursive(string $path, string $pattern): array { */ function pathToUri(string $filepath): string { $filepath = trim(str_replace('\\', '/', $filepath), '/'); - $filepath = implode('/', array_map('urlencode', explode('/', $filepath))); + $parts = explode('/', $filepath); + // Don't %-encode the colon after a Windows drive letter + $first = array_shift($parts); + if (substr($first, -1) !== ':') { + $first = urlencode($first); + } + $parts = array_map('urlencode', $parts); + array_unshift($parts, $first); + $filepath = implode('/', $parts); return 'file:///' . $filepath; } + +/** + * Transforms URI into file path + * + * @param string $uri + * @return string + */ +function uriToPath(string $uri) +{ + $fragments = parse_url($uri); + if ($fragments === null || !isset($fragments['scheme']) || $fragments['scheme'] !== 'file') { + throw new InvalidArgumentException("Not a valid file URI: $uri"); + } + $filepath = urldecode($fragments['path']); + return strpos($filepath, ':') === false ? $filepath : str_replace('/', '\\', $filepath); +} diff --git a/tests/FormatterTest.php b/tests/FormatterTest.php new file mode 100644 index 0000000..a46f2ec --- /dev/null +++ b/tests/FormatterTest.php @@ -0,0 +1,28 @@ +assertSame($output, $edits[0]->newText); + } + + public function testFormatNoChange() + { + $expected = file_get_contents(__DIR__ . '/../fixtures/format_expected.php'); + + $edits = Formatter::format($expected, 'file:///whatever'); + $this->assertSame([], $edits); + } +} diff --git a/tests/Server/TextDocument/FormattingTest.php b/tests/Server/TextDocument/FormattingTest.php index 2dcc46e..957f4a6 100644 --- a/tests/Server/TextDocument/FormattingTest.php +++ b/tests/Server/TextDocument/FormattingTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use LanguageServer\Tests\MockProtocolStream; use LanguageServer\{Server, Client, LanguageClient, Project}; use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, FormattingOptions}; +use function LanguageServer\{pathToUri, uriToPath}; class FormattingTest extends TestCase { @@ -22,24 +23,26 @@ class FormattingTest extends TestCase $this->textDocument = new Server\TextDocument($project, $client); } - public function test() + public function testFormatting() { $client = new LanguageClient(new MockProtocolStream()); $project = new Project($client); $textDocument = new Server\TextDocument($project, $client); + $path = realpath(__DIR__ . '/../../../fixtures/format.php'); + $uri = pathToUri($path); // Trigger parsing of source $textDocumentItem = new TextDocumentItem(); - $textDocumentItem->uri = 'whatever'; + $textDocumentItem->uri = $uri; $textDocumentItem->languageId = 'php'; $textDocumentItem->version = 1; - $textDocumentItem->text = file_get_contents(__DIR__ . '/../../../fixtures/format.php'); + $textDocumentItem->text = file_get_contents($path); $textDocument->didOpen($textDocumentItem); // how code should look after formatting $expected = file_get_contents(__DIR__ . '/../../../fixtures/format_expected.php'); // Request formatting - $result = $textDocument->formatting(new TextDocumentIdentifier('whatever'), new FormattingOptions()); + $result = $textDocument->formatting(new TextDocumentIdentifier($uri), new FormattingOptions()); $this->assertEquals([0 => [ 'range' => [ 'start' => [ @@ -47,11 +50,21 @@ class FormattingTest extends TestCase 'character' => 0 ], 'end' => [ - 'line' => PHP_INT_MAX, - 'character' => PHP_INT_MAX + 'line' => 20, + 'character' => 0 ] ], 'newText' => $expected ]], json_decode(json_encode($result), true)); } + + public function testFormattingInvalidUri() + { + $client = new LanguageClient(new MockProtocolStream()); + $project = new Project($client); + $textDocument = new Server\TextDocument($project, $client); + + $result = $textDocument->formatting(new TextDocumentIdentifier('whatever'), new FormattingOptions()); + $this->assertSame([], $result); + } } diff --git a/tests/Utils/FileUriTest.php b/tests/Utils/FileUriTest.php index 802f78b..a9d3559 100644 --- a/tests/Utils/FileUriTest.php +++ b/tests/Utils/FileUriTest.php @@ -4,33 +4,66 @@ declare(strict_types = 1); namespace LanguageServer\Tests\Utils; use PHPUnit\Framework\TestCase; +use InvalidArgumentException; +use function LanguageServer\{pathToUri, uriToPath}; class FileUriTest extends TestCase { - public function testSpecialCharsAreEscaped() + public function testPathToUri() { - $uri = \LanguageServer\pathToUri('c:/path/to/file/dürüm döner.php'); - $this->assertEquals('file:///c%3A/path/to/file/d%C3%BCr%C3%BCm+d%C3%B6ner.php', $uri); - } - - public function testUriIsWellFormed() - { - $uri = \LanguageServer\pathToUri('var/log'); + $uri = pathToUri('var/log'); $this->assertEquals('file:///var/log', $uri); - $uri = \LanguageServer\pathToUri('/usr/local/bin'); + $uri = pathToUri('/usr/local/bin'); $this->assertEquals('file:///usr/local/bin', $uri); - $uri = \LanguageServer\pathToUri('a/b/c/test.txt'); + $uri = pathToUri('a/b/c/test.txt'); $this->assertEquals('file:///a/b/c/test.txt', $uri); - $uri = \LanguageServer\pathToUri('/d/e/f'); + $uri = pathToUri('/d/e/f'); $this->assertEquals('file:///d/e/f', $uri); + + // special chars are escaped + $uri = pathToUri('c:/path/to/file/dürüm döner.php'); + $this->assertEquals('file:///c:/path/to/file/d%C3%BCr%C3%BCm+d%C3%B6ner.php', $uri); + + //backslashes are transformed + $uri = pathToUri('c:\\foo\\bar.baz'); + $this->assertEquals('file:///c:/foo/bar.baz', $uri); } - public function testBackslashesAreTransformed() + public function testUriToPath() { - $uri = \LanguageServer\pathToUri('c:\\foo\\bar.baz'); - $this->assertEquals('file:///c%3A/foo/bar.baz', $uri); + $uri = 'file:///var/log'; + $this->assertEquals('/var/log', uriToPath($uri)); + + $uri = 'file:///usr/local/bin'; + $this->assertEquals('/usr/local/bin', uriToPath($uri)); + + $uri = 'file:///a/b/c/test.txt'; + $this->assertEquals('/a/b/c/test.txt', uriToPath($uri)); + + $uri = 'file:///d/e/f'; + $this->assertEquals('/d/e/f', uriToPath($uri)); + + $uri = 'file:///c:/path/to/file/d%C3%BCr%C3%BCm+d%C3%B6ner.php'; + $this->assertEquals('c:\\path\\to\\file\\dürüm döner.php', uriToPath($uri)); + + $uri = 'file:///c:/foo/bar.baz'; + $this->assertEquals('c:\\foo\\bar.baz', uriToPath($uri)); + } + + public function testUriToPathForUnknownProtocol() + { + $this->expectException(InvalidArgumentException::class); + $uri = 'vfs:///whatever'; + uriToPath($uri); + } + + public function testUriToPathForInvalidProtocol() + { + $this->expectException(InvalidArgumentException::class); + $uri = 'http://www.google.com'; + uriToPath($uri); } }