1
0
Fork 0

Merge branch 'master' into signature-help

pull/438/head
Ivan Bozhanov 2017-11-20 09:47:20 +02:00 committed by GitHub
commit 10efca932b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 336 additions and 108 deletions

View File

@ -23,7 +23,7 @@ install:
- composer install --prefer-dist --no-interaction
script:
- vendor/bin/phpcs -n
- vendor/bin/phpunit --coverage-clover=coverage.xml
- vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always
- bash <(curl -s https://codecov.io/bash)
jobs:

52
.vscode/launch.json vendored
View File

@ -1,29 +1,27 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9000
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 9000
},
{
"name": "PHPUnit",
"type": "php",
"request": "launch",
"program": "${workspaceRoot}/vendor/phpunit/phpunit/phpunit",
"cwd": "${workspaceRoot}",
"args": [
// "--filter", "CompletionTest"
]
}
]
"version": "0.2.0",
"configurations": [
{
"name": "PHPUnit",
"type": "php",
"request": "launch",
"program": "${workspaceRoot}/vendor/phpunit/phpunit/phpunit",
// "args": ["--filter", "testDefinitionForSelfKeyword"],
"cwd": "${workspaceRoot}"
},
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9000
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 9000
}
]
}

View File

@ -0,0 +1,9 @@
<?php
class Foo
{
public function bar()
{
return $this;
}
}

View File

@ -0,0 +1,6 @@
<?php
function foo()
{
return $this;
}

View File

@ -0,0 +1,3 @@
<?php
echo $this;

View File

@ -0,0 +1,9 @@
<?php
class Foo
{
public static function bar()
{
return $this;
}
}

View File

@ -282,13 +282,38 @@ class DefinitionResolver
// Other references are references to a global symbol that have an FQN
// Find out the FQN
$fqn = $this->resolveReferenceNodeToFqn($node);
if ($fqn === null) {
if (!$fqn) {
return null;
}
if ($fqn === 'self' || $fqn === 'static') {
// Resolve self and static keywords to the containing class
// (This is not 100% correct for static but better than nothing)
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class);
if (!$classNode) {
return;
}
$fqn = (string)$classNode->getNamespacedName();
if (!$fqn) {
return;
}
} else if ($fqn === 'parent') {
// Resolve parent keyword to the base class FQN
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class);
if (!$classNode || !$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) {
return;
}
$fqn = (string)$classNode->classBaseClause->baseClass->getResolvedName();
if (!$fqn) {
return;
}
}
// If the node is a function or constant, it could be namespaced, but PHP falls back to global
// http://php.net/manual/en/language.namespaces.fallback.php
// TODO - verify that this is not a method
$globalFallback = ParserHelpers\isConstantFetch($node) || $parent instanceof Node\Expression\CallExpression;
// Return the Definition object from the index index
return $this->index->getDefinition($fqn, $globalFallback);
}
@ -296,6 +321,7 @@ class DefinitionResolver
/**
* Given any node, returns the FQN of the symbol that is referenced
* Returns null if the FQN could not be resolved or the reference node references a variable
* May also return "static", "self" or "parent"
*
* @param Node $node
* @return string|null

View File

@ -68,6 +68,7 @@ class TreeAnalyzer
*/
private function collectDiagnostics($node)
{
// Get errors from the parser.
if (($error = PhpParser\DiagnosticsProvider::checkDiagnostics($node)) !== null) {
$range = PhpParser\PositionUtilities::getRangeFromPosition($error->start, $error->length, $this->sourceFileNode->fileContents);
@ -92,6 +93,24 @@ class TreeAnalyzer
'php'
);
}
// Check for invalid usage of $this.
if ($node instanceof Node\Expression\Variable && $node->getName() === 'this') {
// Find the first ancestor that's a class method. Return an error
// if there is none, or if the method is static.
$method = $node->getFirstAncestor(Node\MethodDeclaration::class);
if ($method === null || $method->isStatic()) {
$this->diagnostics[] = new Diagnostic(
$method === null
? "\$this can only be used in an object context."
: "\$this can not be used in static methods.",
Range::fromNode($node),
null,
DiagnosticSeverity::ERROR,
'php'
);
}
}
}
/**
@ -140,8 +159,9 @@ class TreeAnalyzer
$this->definitionNodes[$fqn] = $node;
$this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn);
} else {
$parent = $node->parent;
if (!(
if (
(
// $node->parent instanceof Node\Expression\ScopedPropertyAccessExpression ||
($node instanceof Node\Expression\ScopedPropertyAccessExpression ||
@ -150,41 +170,68 @@ class TreeAnalyzer
$node->parent instanceof Node\Expression\CallExpression ||
$node->memberName instanceof PhpParser\Token
))
|| ($parent instanceof Node\Statement\NamespaceDefinition && $parent->name !== null && $parent->name->getStart() === $node->getStart()))
|| ($parent instanceof Node\Statement\NamespaceDefinition && $parent->name !== null && $parent->name->getStart() === $node->getStart())
) {
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
if ($fqn !== null) {
$this->addReference($fqn, $node);
return;
}
if (
$node instanceof Node\QualifiedName
&& ($node->isQualifiedName() || $node->parent instanceof Node\NamespaceUseClause)
&& !($parent instanceof Node\Statement\NamespaceDefinition && $parent->name->getStart() === $node->getStart()
)
) {
// Add references for each referenced namespace
$ns = $fqn;
while (($pos = strrpos($ns, '\\')) !== false) {
$ns = substr($ns, 0, $pos);
$this->addReference($ns, $node);
}
}
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
if (!$fqn) {
return;
}
// Namespaced constant access and function calls also need to register a reference
// to the global version because PHP falls back to global at runtime
// http://php.net/manual/en/language.namespaces.fallback.php
if (ParserHelpers\isConstantFetch($node) ||
($parent instanceof Node\Expression\CallExpression
&& !(
$node instanceof Node\Expression\ScopedPropertyAccessExpression ||
$node instanceof Node\Expression\MemberAccessExpression
))) {
$parts = explode('\\', $fqn);
if (count($parts) > 1) {
$globalFqn = end($parts);
$this->addReference($globalFqn, $node);
}
}
if ($fqn === 'self' || $fqn === 'static') {
// Resolve self and static keywords to the containing class
// (This is not 100% correct for static but better than nothing)
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class);
if (!$classNode) {
return;
}
$fqn = (string)$classNode->getNamespacedName();
if (!$fqn) {
return;
}
} else if ($fqn === 'parent') {
// Resolve parent keyword to the base class FQN
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class);
if (!$classNode || !$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) {
return;
}
$fqn = (string)$classNode->classBaseClause->baseClass->getResolvedName();
if (!$fqn) {
return;
}
}
$this->addReference($fqn, $node);
if (
$node instanceof Node\QualifiedName
&& ($node->isQualifiedName() || $node->parent instanceof Node\NamespaceUseClause)
&& !($parent instanceof Node\Statement\NamespaceDefinition && $parent->name->getStart() === $node->getStart()
)
) {
// Add references for each referenced namespace
$ns = $fqn;
while (($pos = strrpos($ns, '\\')) !== false) {
$ns = substr($ns, 0, $pos);
$this->addReference($ns, $node);
}
}
// Namespaced constant access and function calls also need to register a reference
// to the global version because PHP falls back to global at runtime
// http://php.net/manual/en/language.namespaces.fallback.php
if (ParserHelpers\isConstantFetch($node) ||
($parent instanceof Node\Expression\CallExpression
&& !(
$node instanceof Node\Expression\ScopedPropertyAccessExpression ||
$node instanceof Node\Expression\MemberAccessExpression
))) {
$parts = explode('\\', $fqn);
if (count($parts) > 1) {
$globalFqn = end($parts);
$this->addReference($globalFqn, $node);
}
}
}

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Diagnostics;
use PHPUnit\Framework\TestCase;
use phpDocumentor\Reflection\DocBlockFactory;
use LanguageServer\{
DefinitionResolver, TreeAnalyzer
};
use LanguageServer\Index\{Index};
use LanguageServer\Protocol\{
Diagnostic, DiagnosticSeverity, Position, Range
};
use function LanguageServer\pathToUri;
use Microsoft\PhpParser\Parser;
class InvalidThisUsageTest extends TestCase
{
/**
* Parse the given file and return diagnostics.
*
* @param string $path
* @return Diagnostic[]
*/
private function collectDiagnostics(string $path): array
{
$uri = pathToUri($path);
$parser = new Parser();
$docBlockFactory = DocBlockFactory::createInstance();
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$content = file_get_contents($path);
$treeAnalyzer = new TreeAnalyzer($parser, $content, $docBlockFactory, $definitionResolver, $uri);
return $treeAnalyzer->getDiagnostics();
}
/**
* Assertions about a diagnostic.
*
* @param Diagnostic|null $diagnostic
* @param int $message
* @param string $severity
* @param Range $range
*/
private function assertDiagnostic($diagnostic, $message, $severity, $range)
{
$this->assertInstanceOf(Diagnostic::class, $diagnostic);
$this->assertEquals($message, $diagnostic->message);
$this->assertEquals($severity, $diagnostic->severity);
$this->assertEquals($range, $diagnostic->range);
}
public function testThisInStaticMethodProducesError()
{
$diagnostics = $this->collectDiagnostics(
__DIR__ . '/../../fixtures/diagnostics/errors/this_in_static_method.php'
);
$this->assertCount(1, $diagnostics);
$this->assertDiagnostic(
$diagnostics[0],
'$this can not be used in static methods.',
DiagnosticSeverity::ERROR,
new Range(
new Position(6, 15),
new Position(6, 20)
)
);
}
public function testThisInFunctionProducesError()
{
$diagnostics = $this->collectDiagnostics(
__DIR__ . '/../../fixtures/diagnostics/errors/this_in_function.php'
);
$this->assertCount(1, $diagnostics);
$this->assertDiagnostic(
$diagnostics[0],
'$this can only be used in an object context.',
DiagnosticSeverity::ERROR,
new Range(
new Position(4, 11),
new Position(4, 16)
)
);
}
public function testThisInRoot()
{
$diagnostics = $this->collectDiagnostics(
__DIR__ . '/../../fixtures/diagnostics/errors/this_in_root.php'
);
$this->assertCount(1, $diagnostics);
$this->assertDiagnostic(
$diagnostics[0],
'$this can only be used in an object context.',
DiagnosticSeverity::ERROR,
new Range(
new Position(2, 5),
new Position(2, 10)
)
);
}
public function testThisInMethodProducesNoError()
{
$diagnostics = $this->collectDiagnostics(
__DIR__ . '/../../fixtures/diagnostics/baselines/this_in_method.php'
);
$this->assertCount(0, $diagnostics);
}
}

View File

@ -122,15 +122,16 @@ abstract class ServerTestCase extends TestCase
0 => new Location($referencesUri, new Range(new Position(29, 5), new Position(29, 15)))
],
'TestNamespace\\TestClass' => [
0 => new Location($symbolsUri , new Range(new Position(99, 25), new Position(99, 34))), // class ChildClass extends TestClass {}
1 => new Location($referencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass();
2 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod();
3 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty;
4 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
5 => new Location($referencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param)
6 => new Location($referencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass
7 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty;
8 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass;
0 => new Location($symbolsUri, new Range(new Position(48, 13), new Position(48, 17))), // echo self::TEST_CLASS_CONST;
1 => new Location($symbolsUri , new Range(new Position(99, 25), new Position(99, 34))), // class ChildClass extends TestClass {}
2 => new Location($referencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass();
3 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod();
4 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty;
5 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
6 => new Location($referencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param)
7 => new Location($referencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass
8 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty;
9 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass;
],
'TestNamespace\\TestChild' => [
0 => new Location($referencesUri, new Range(new Position(42, 5), new Position(42, 25))), // echo $child->testProperty;
@ -176,14 +177,15 @@ abstract class ServerTestCase extends TestCase
1 => new Location($globalReferencesUri, new Range(new Position(29, 5), new Position(29, 15)))
],
'TestClass' => [
0 => new Location($globalSymbolsUri, new Range(new Position(99, 25), new Position(99, 34))), // class ChildClass extends TestClass {}
1 => new Location($globalReferencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass();
2 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod();
3 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty;
4 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
5 => new Location($globalReferencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param)
6 => new Location($globalReferencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass
7 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty;
0 => new Location($globalSymbolsUri, new Range(new Position(48, 13), new Position(48, 17))), // echo self::TEST_CLASS_CONST;
1 => new Location($globalSymbolsUri, new Range(new Position(99, 25), new Position(99, 34))), // class ChildClass extends TestClass {}
2 => new Location($globalReferencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass();
3 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod();
4 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty;
5 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
6 => new Location($globalReferencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param)
7 => new Location($globalReferencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass
8 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty;
],
'TestChild' => [
0 => new Location($globalReferencesUri, new Range(new Position(42, 5), new Position(42, 25))), // echo $child->testProperty;

View File

@ -29,11 +29,23 @@ class GlobalTest extends ServerTestCase
$this->assertEquals([], $result);
}
public function testDefinitionForSelfKeyword()
{
// echo self::TEST_CLASS_CONST;
// Get definition for self
$reference = $this->getReferenceLocations('TestClass')[0];
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass'), $result);
}
public function testDefinitionForClassLike()
{
// $obj = new TestClass();
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[0];
$reference = $this->getReferenceLocations('TestClass')[1];
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
@ -45,7 +57,7 @@ class GlobalTest extends ServerTestCase
{
// TestClass::staticTestMethod();
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[1];
$reference = $this->getReferenceLocations('TestClass')[2];
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
@ -57,7 +69,7 @@ class GlobalTest extends ServerTestCase
{
// echo TestClass::$staticTestProperty;
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[2];
$reference = $this->getReferenceLocations('TestClass')[3];
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
@ -69,7 +81,7 @@ class GlobalTest extends ServerTestCase
{
// TestClass::TEST_CLASS_CONST;
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[3];
$reference = $this->getReferenceLocations('TestClass')[4];
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
@ -213,7 +225,7 @@ class GlobalTest extends ServerTestCase
{
// function whatever(TestClass $param) {
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[4];
$reference = $this->getReferenceLocations('TestClass')[5];
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
@ -225,7 +237,7 @@ class GlobalTest extends ServerTestCase
{
// function whatever(TestClass $param): TestClass {
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[5];
$reference = $this->getReferenceLocations('TestClass')[6];
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start

View File

@ -34,7 +34,7 @@ class NamespacedTest extends GlobalTest
{
// use TestNamespace\TestClass;
// Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[6];
$reference = $this->getReferenceLocations('TestClass')[7];
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
@ -46,7 +46,7 @@ class NamespacedTest extends GlobalTest
{
// use TestNamespace\{TestTrait, TestInterface};
// Get definition for TestInterface
$reference = $this->getReferenceLocations('TestClass')[0];
$reference = $this->getReferenceLocations('TestClass')[1];
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start

View File

@ -15,7 +15,7 @@ class HoverTest extends ServerTestCase
{
// $obj = new TestClass();
// Get hover for TestClass
$reference = $this->getReferenceLocations('TestClass')[0];
$reference = $this->getReferenceLocations('TestClass')[1];
$result = $this->textDocument->hover(
new TextDocumentIdentifier($reference->uri),
$reference->range->start

View File

@ -151,7 +151,7 @@ class GlobalTest extends ServerTestCase
{
// $obj = new TestClass();
// Get references for TestClass
$reference = $this->getReferenceLocations('TestClass')[0];
$reference = $this->getReferenceLocations('TestClass')[1];
$result = $this->textDocument->references(
new ReferenceContext,
new TextDocumentIdentifier($reference->uri),

View File

@ -3,7 +3,7 @@
"Fixtures\\Prophecy\\EmptyClass": [
"./WithReturnTypehints.php"
],
"self": [
"Fixtures\\Prophecy\\WithReturnTypehints": [
"./WithReturnTypehints.php"
],
"Fixtures\\Prophecy\\__CLASS__": [
@ -11,9 +11,6 @@
],
"__CLASS__": [
"./WithReturnTypehints.php"
],
"parent": [
"./WithReturnTypehints.php"
]
},
"definitions": {

View File

@ -1,5 +1,9 @@
{
"references": [],
"references": {
"A": [
"./nameToken.php"
]
},
"definitions": {
"A": {
"fqn": "A",

View File

@ -2,9 +2,6 @@
"references": {
"MyNamespace\\B": [
"./parent1.php"
],
"parent": [
"./parent1.php"
]
},
"definitions": {

View File

@ -5,9 +5,6 @@
],
"MyNamespace\\B->b()": [
"./parent3.php"
],
"parent": [
"./parent3.php"
]
},
"definitions": {

View File

@ -6,7 +6,7 @@
"MyNamespace\\A::b()": [
"./self1.php"
],
"self": [
"MyNamespace\\A": [
"./self1.php"
]
},

View File

@ -6,7 +6,7 @@
"MyNamespace\\A::b()": [
"./self2.php"
],
"self": [
"MyNamespace\\A": [
"./self2.php"
]
},

View File

@ -6,7 +6,7 @@
"MyNamespace\\A->b()": [
"./self3.php"
],
"self": [
"MyNamespace\\A": [
"./self3.php"
]
},

View File

@ -1,6 +1,6 @@
{
"references": {
"self": [
"MyNamespace\\A": [
"./self4.php"
],
"MyNamespace\\A->addTestFile()": [

View File

@ -6,7 +6,7 @@
"MyNamespace\\A::b()": [
"./static1.php"
],
"static": [
"MyNamespace\\A": [
"./static1.php"
]
},

View File

@ -6,7 +6,7 @@
"MyNamespace\\A::b()": [
"./static2.php"
],
"static": [
"MyNamespace\\A": [
"./static2.php"
]
},

View File

@ -3,10 +3,10 @@
"MyNamespace\\B": [
"./static3.php"
],
"MyNamespace\\b()": [
"static->b()": [
"./static3.php"
],
"b()": [
"MyNamespace\\A": [
"./static3.php"
]
},

View File

@ -2,6 +2,9 @@
"references": {
"MyNamespace\\B": [
"./static4.php"
],
"MyNamespace\\A": [
"./static4.php"
]
},
"definitions": {