parent
18ac760bc6
commit
e75c1592fc
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue