1
0
Fork 0

Use PHP_CodeSniffer as a formatter (#35)

Also adds uriTopath util function
pull/60/merge
Michal Niewrzal 2016-10-10 15:06:02 +02:00 committed by Felix Becker
parent 18ac760bc6
commit e75c1592fc
10 changed files with 232 additions and 36 deletions

View File

@ -27,7 +27,8 @@
"nikic/php-parser": "^3.0.0beta1", "nikic/php-parser": "^3.0.0beta1",
"phpdocumentor/reflection-docblock": "^3.0", "phpdocumentor/reflection-docblock": "^3.0",
"sabre/event": "^4.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", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,

View File

@ -1,13 +1,13 @@
<?php <?php
namespace TestNamespace; namespace TestNamespace;
use SomeNamespace\Goo;
class TestClass
class TestClass
{ {
public $testProperty; public $testProperty;
public function testMethod($testParameter) public function testMethod($testParameter)
{ {

View File

@ -2,14 +2,18 @@
namespace TestNamespace; namespace TestNamespace;
use SomeNamespace\Goo;
class TestClass class TestClass
{ {
public $testProperty; public $testProperty;
public function testMethod($testParameter) public function testMethod($testParameter)
{ {
$testVariable = 123; $testVariable = 123;
if (empty($testParameter)) { if (empty($testParameter)) {
echo 'Empty'; echo 'Empty';
} }
} }
} }

89
src/Formatter.php Normal file
View File

@ -0,0 +1,89 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Protocol\ {
TextEdit,
Range,
Position
};
use PHP_CodeSniffer;
use Exception;
abstract class Formatter
{
/**
* Generate array of TextEdit changes for content formatting.
*
* @param string $content source code to format
* @param string $uri URI of document
*
* @return \LanguageServer\Protocol\TextEdit[]
* @throws \Exception
*/
public static function format(string $content, string $uri)
{
$path = uriToPath($uri);
$cs = new PHP_CodeSniffer();
$cs->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);
}
}

View File

@ -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() public function getFormattedText()
{ {
if (empty($this->stmts)) { if (empty($this->getContent())) {
return []; return [];
} }
$prettyPrinter = new PrettyPrinter(); return Formatter::format($this->content, $this->uri);
$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];
} }
/** /**

View File

@ -22,4 +22,10 @@ class TextEdit
* @var string * @var string
*/ */
public $newText; public $newText;
public function __construct(Range $range = null, string $newText = null)
{
$this->range = $range;
$this->newText = $newText;
}
} }

View File

@ -3,6 +3,8 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use InvalidArgumentException;
/** /**
* Recursively Searches files with matching filename, starting at $path. * 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 { function pathToUri(string $filepath): string {
$filepath = trim(str_replace('\\', '/', $filepath), '/'); $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; 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);
}

28
tests/FormatterTest.php Normal file
View File

@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase;
use LanguageServer\Formatter;
class FormatterTest extends TestCase
{
public function testFormat()
{
$input = file_get_contents(__DIR__ . '/../fixtures/format.php');
$output = file_get_contents(__DIR__ . '/../fixtures/format_expected.php');
$edits = Formatter::format($input, 'file:///whatever');
$this->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);
}
}

View File

@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project}; use LanguageServer\{Server, Client, LanguageClient, Project};
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, FormattingOptions}; use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, FormattingOptions};
use function LanguageServer\{pathToUri, uriToPath};
class FormattingTest extends TestCase class FormattingTest extends TestCase
{ {
@ -22,24 +23,26 @@ class FormattingTest extends TestCase
$this->textDocument = new Server\TextDocument($project, $client); $this->textDocument = new Server\TextDocument($project, $client);
} }
public function test() public function testFormatting()
{ {
$client = new LanguageClient(new MockProtocolStream()); $client = new LanguageClient(new MockProtocolStream());
$project = new Project($client); $project = new Project($client);
$textDocument = new Server\TextDocument($project, $client); $textDocument = new Server\TextDocument($project, $client);
$path = realpath(__DIR__ . '/../../../fixtures/format.php');
$uri = pathToUri($path);
// Trigger parsing of source // Trigger parsing of source
$textDocumentItem = new TextDocumentItem(); $textDocumentItem = new TextDocumentItem();
$textDocumentItem->uri = 'whatever'; $textDocumentItem->uri = $uri;
$textDocumentItem->languageId = 'php'; $textDocumentItem->languageId = 'php';
$textDocumentItem->version = 1; $textDocumentItem->version = 1;
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../../fixtures/format.php'); $textDocumentItem->text = file_get_contents($path);
$textDocument->didOpen($textDocumentItem); $textDocument->didOpen($textDocumentItem);
// how code should look after formatting // how code should look after formatting
$expected = file_get_contents(__DIR__ . '/../../../fixtures/format_expected.php'); $expected = file_get_contents(__DIR__ . '/../../../fixtures/format_expected.php');
// Request formatting // Request formatting
$result = $textDocument->formatting(new TextDocumentIdentifier('whatever'), new FormattingOptions()); $result = $textDocument->formatting(new TextDocumentIdentifier($uri), new FormattingOptions());
$this->assertEquals([0 => [ $this->assertEquals([0 => [
'range' => [ 'range' => [
'start' => [ 'start' => [
@ -47,11 +50,21 @@ class FormattingTest extends TestCase
'character' => 0 'character' => 0
], ],
'end' => [ 'end' => [
'line' => PHP_INT_MAX, 'line' => 20,
'character' => PHP_INT_MAX 'character' => 0
] ]
], ],
'newText' => $expected 'newText' => $expected
]], json_decode(json_encode($result), true)); ]], 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);
}
} }

View File

@ -4,33 +4,66 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Utils; namespace LanguageServer\Tests\Utils;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use InvalidArgumentException;
use function LanguageServer\{pathToUri, uriToPath};
class FileUriTest extends TestCase class FileUriTest extends TestCase
{ {
public function testSpecialCharsAreEscaped() public function testPathToUri()
{ {
$uri = \LanguageServer\pathToUri('c:/path/to/file/dürüm döner.php'); $uri = pathToUri('var/log');
$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');
$this->assertEquals('file:///var/log', $uri); $this->assertEquals('file:///var/log', $uri);
$uri = \LanguageServer\pathToUri('/usr/local/bin'); $uri = pathToUri('/usr/local/bin');
$this->assertEquals('file:///usr/local/bin', $uri); $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); $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); $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'); $uri = 'file:///var/log';
$this->assertEquals('file:///c%3A/foo/bar.baz', $uri); $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);
} }
} }