Skip to content

Use PHP_CodeSniffer as a formatter #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Oct 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions fixtures/format.php
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<?php

namespace TestNamespace;
namespace TestNamespace;

use SomeNamespace\Goo;



class TestClass
class TestClass
{
public $testProperty;
public $testProperty;

public function testMethod($testParameter)
{
Expand Down
6 changes: 5 additions & 1 deletion fixtures/format_expected.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

namespace TestNamespace;

use SomeNamespace\Goo;

class TestClass
{
public $testProperty;

public function testMethod($testParameter)
{
$testVariable = 123;

if (empty($testParameter)) {
echo 'Empty';
}
}
}
}
89 changes: 89 additions & 0 deletions src/Formatter.php
Original file line number Diff line number Diff line change
@@ -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);
}

}
12 changes: 4 additions & 8 deletions src/PhpDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,20 +218,16 @@ public function parse()
}

/**
* 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);
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/Protocol/TextEdit.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
28 changes: 27 additions & 1 deletion src/utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

namespace LanguageServer;

use InvalidArgumentException;

/**
* Recursively Searches files with matching filename, starting at $path.
*
Expand All @@ -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);
Copy link
Owner

@felixfbecker felixfbecker Oct 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little brittle. There are also implementations that use | instead of the colon on windows. You don't need to test against that, but it would be better to simply do str_replace('/', DIRECTORY_SEPERATOR, $filepath)

https://en.wikipedia.org/wiki/File_URI_scheme#Windows

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forget what I said. the directory separator should of course be determined by the URI, not the runtime OS.

}
28 changes: 28 additions & 0 deletions tests/FormatterTest.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
25 changes: 19 additions & 6 deletions tests/Server/TextDocument/FormattingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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
{
Expand All @@ -22,36 +23,48 @@ public function setUp()
$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' => [
'line' => 0,
'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);
}
}
61 changes: 47 additions & 14 deletions tests/Utils/FileUriTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,66 @@
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 testUriToPath()
{
$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 testBackslashesAreTransformed()
public function testUriToPathForInvalidProtocol()
{
$uri = \LanguageServer\pathToUri('c:\\foo\\bar.baz');
$this->assertEquals('file:///c%3A/foo/bar.baz', $uri);
$this->expectException(InvalidArgumentException::class);
$uri = 'http://www.google.com';
uriToPath($uri);
}
}