1
0
Fork 0

Adopt Microsoft/tolerant-php-parser (#357)

pull/395/head
Sara Itani 2017-06-09 14:25:30 -04:00 committed by Felix Becker
parent 08cf1a3fd7
commit 7f427a1215
194 changed files with 6468 additions and 1501 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ vendor/
.phpls/
composer.lock
stubs
*.ast

27
.gitmodules vendored Normal file
View File

@ -0,0 +1,27 @@
[submodule "validation/frameworks/php-language-server"]
path = validation/frameworks/php-language-server
url = https://github.com/felixfbecker/php-language-server
[submodule "validation/frameworks/wordpress"]
path = validation/frameworks/wordpress
url = https://github.com/wordpress/wordpress
[submodule "validation/frameworks/drupal"]
path = validation/frameworks/drupal
url = https://github.com/drupal/drupal
[submodule "validation/frameworks/tolerant-php-parser"]
path = validation/frameworks/tolerant-php-parser
url = https://github.com/microsoft/tolerant-php-parser
[submodule "validation/frameworks/symfony"]
path = validation/frameworks/symfony
url = https://github.com/symfony/symfony
[submodule "validation/frameworks/math-php"]
path = validation/frameworks/math-php
url = https://github.com/markrogoyski/math-php
[submodule "validation/frameworks/codeigniter"]
path = validation/frameworks/codeigniter
url = https://github.com/bcit-ci/codeigniter
[submodule "validation/frameworks/cakephp"]
path = validation/frameworks/cakephp
url = https://github.com/cakephp/cakephp
[submodule "validation/frameworks/phpunit"]
path = validation/frameworks/phpunit
url = https://github.com/sebastianbergmann/phpunit

65
Performance.php Normal file
View File

@ -0,0 +1,65 @@
<?php
namespace LanguageServer\Tests;
require __DIR__ . '/vendor/autoload.php';
use Exception;
use LanguageServer\Index\Index;
use LanguageServer\PhpDocument;
use LanguageServer\DefinitionResolver;
use Microsoft\PhpParser;
use phpDocumentor\Reflection\DocBlockFactory;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
$totalSize = 0;
$frameworks = ["drupal", "wordpress", "php-language-server", "tolerant-php-parser", "math-php", "symfony", "CodeIgniter", "cakephp"];
foreach($frameworks as $framework) {
$iterator = new RecursiveDirectoryIterator(__DIR__ . "/validation/frameworks/$framework");
$testProviderArray = array();
foreach (new RecursiveIteratorIterator($iterator) as $file) {
if (strpos((string)$file, ".php") !== false) {
$totalSize += $file->getSize();
$testProviderArray[] = $file->getPathname();
}
}
if (count($testProviderArray) === 0) {
throw new Exception("ERROR: Validation testsuite frameworks not found - run `git submodule update --init --recursive` to download.");
}
$start = microtime(true);
foreach ($testProviderArray as $idx => $testCaseFile) {
if (filesize($testCaseFile) > 10000) {
continue;
}
if ($idx % 1000 === 0) {
echo "$idx\n";
}
$fileContents = file_get_contents($testCaseFile);
$docBlockFactory = DocBlockFactory::createInstance();
$index = new Index;
$maxRecursion = [];
$definitions = [];
$definitionResolver = new DefinitionResolver($index);
$parser = new PhpParser\Parser();
try {
$document = new PhpDocument($testCaseFile, $fileContents, $index, $parser, $docBlockFactory, $definitionResolver);
} catch (\Throwable $e) {
continue;
}
}
echo "------------------------------\n";
echo "Time [$framework]: " . (microtime(true) - $start) . PHP_EOL;
}

View File

@ -28,7 +28,6 @@
},
"require": {
"php": ">=7.0",
"nikic/php-parser": "^3.0.5",
"phpdocumentor/reflection-docblock": "^3.0",
"sabre/event": "^5.0",
"felixfbecker/advanced-json-rpc": "^2.0",
@ -38,7 +37,8 @@
"webmozart/glob": "^4.1",
"sabre/uri": "^2.0",
"jetbrains/phpstorm-stubs": "dev-master",
"composer/composer": "^1.3"
"composer/composer": "^1.3",
"Microsoft/tolerant-php-parser": "^0.0.2"
},
"minimum-stability": "dev",
"prefer-stable": true,
@ -47,7 +47,9 @@
"LanguageServer\\": "src/"
},
"files" : [
"src/utils.php"
"src/utils.php",
"src/FqnUtilities.php",
"src/ParserHelpers.php"
]
},
"autoload-dev": {

View File

@ -2,6 +2,7 @@
<ruleset name="PHP Language Server">
<file>src</file>
<file>tests</file>
<exclude-pattern>tests/Validation/cases</exclude-pattern>
<rule ref="PSR2">
<exclude name="PSR2.Namespaces.UseDeclaration.MultipleDeclarations"/>
<exclude name="PSR2.ControlStructures.ElseIfDeclaration.NotAllowed"/>

View File

@ -12,6 +12,6 @@
</whitelist>
</filter>
<php>
<ini name="memory_limit" value="256M"/>
<ini name="memory_limit" value="1024M"/>
</php>
</phpunit>

View File

@ -3,7 +3,6 @@ declare(strict_types = 1);
namespace LanguageServer;
use PhpParser\Node;
use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{
TextEdit,
@ -13,6 +12,8 @@ use LanguageServer\Protocol\{
CompletionItem,
CompletionItemKind
};
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
class CompletionProvider
{
@ -104,7 +105,7 @@ class CompletionProvider
/**
* @param DefinitionResolver $definitionResolver
* @param ReadableIndex $index
* @param ReadableIndex $index
*/
public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index)
{
@ -121,45 +122,74 @@ class CompletionProvider
*/
public function provideCompletion(PhpDocument $doc, Position $pos): CompletionList
{
// This can be made much more performant if the tree follows specific invariants.
$node = $doc->getNodeAtPosition($pos);
if ($node instanceof Node\Expr\Error) {
$node = $node->getAttribute('parentNode');
$offset = $node === null ? -1 : $pos->toOffset($node->getFileContents());
if (
$node !== null
&& $offset > $node->getEndPosition()
&& $node->parent !== null
&& $node->parent->getLastChild() instanceof PhpParser\MissingToken
) {
$node = $node->parent;
}
$list = new CompletionList;
$list->isIncomplete = true;
// A non-free node means we do NOT suggest global symbols
if (
$node instanceof Node\Expr\MethodCall
|| $node instanceof Node\Expr\PropertyFetch
|| $node instanceof Node\Expr\StaticCall
|| $node instanceof Node\Expr\StaticPropertyFetch
|| $node instanceof Node\Expr\ClassConstFetch
if ($node instanceof Node\Expression\Variable &&
$node->parent instanceof Node\Expression\ObjectCreationExpression &&
$node->name instanceof PhpParser\MissingToken
) {
// If the name is an Error node, just filter by the class
if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
// For instances, resolve the variable type
$prefixes = DefinitionResolver::getFqnsFromType(
$this->definitionResolver->resolveExpressionNodeToType($node->var)
$node = $node->parent;
}
if ($node === null || $node instanceof Node\Statement\InlineHtml || $pos == new Position(0, 0)) {
$item = new CompletionItem('<?php', CompletionItemKind::KEYWORD);
$item->textEdit = new TextEdit(
new Range($pos, $pos),
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), '<?php')
);
$list->items[] = $item;
} /*
VARIABLES */
elseif (
$node instanceof Node\Expression\Variable &&
!(
$node->parent instanceof Node\Expression\ScopedPropertyAccessExpression &&
$node->parent->memberName === $node)
) {
// Find variables, parameters and use statements in the scope
$namePrefix = $node->getName() ?? '';
foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) {
$item = new CompletionItem;
$item->kind = CompletionItemKind::VARIABLE;
$item->label = '$' . $var->getName();
$item->documentation = $this->definitionResolver->getDocumentationFromNode($var);
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var);
$item->textEdit = new TextEdit(
new Range($pos, $pos),
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), $item->label)
);
} else {
// Static member reference
$prefixes = [$node->class instanceof Node\Name ? (string)$node->class : ''];
$list->items[] = $item;
}
} /*
MEMBER ACCESS EXPRESSIONS
$a->c#
$a-># */
elseif ($node instanceof Node\Expression\MemberAccessExpression) {
$prefixes = FqnUtilities\getFqnsFromType(
$this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression)
);
$prefixes = $this->expandParentFqns($prefixes);
// If we are just filtering by the class, add the appropiate operator to the prefix
// to filter the type of symbol
foreach ($prefixes as &$prefix) {
if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
$prefix .= '->';
} else if ($node instanceof Node\Expr\StaticCall || $node instanceof Node\Expr\ClassConstFetch) {
$prefix .= '::';
} else if ($node instanceof Node\Expr\StaticPropertyFetch) {
$prefix .= '::$';
}
$prefix .= '->';
}
unset($prefix);
foreach ($this->index->getDefinitions() as $fqn => $def) {
@ -169,122 +199,114 @@ class CompletionProvider
}
}
}
} else if (
// A ConstFetch means any static reference, like a class, interface, etc. or keyword
($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch)
|| $node instanceof Node\Expr\New_
} /*
SCOPED PROPERTY ACCESS EXPRESSIONS
A\B\C::$a#
A\B\C::#
A\B\C::$#
A\B\C::foo#
TODO: $a::# */
elseif (
($scoped = $node->parent) instanceof Node\Expression\ScopedPropertyAccessExpression ||
($scoped = $node) instanceof Node\Expression\ScopedPropertyAccessExpression
) {
$prefix = '';
$prefixLen = 0;
if ($node instanceof Node\Name) {
$isFullyQualified = $node->isFullyQualified();
$prefix = (string)$node;
$prefixLen = strlen($prefix);
$namespacedPrefix = (string)$node->getAttribute('namespacedName');
$namespacedPrefixLen = strlen($prefix);
$prefixes = FqnUtilities\getFqnsFromType(
$classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier)
);
$prefixes = $this->expandParentFqns($prefixes);
foreach ($prefixes as &$prefix) {
$prefix .= '::';
}
// Find closest namespace
$namespace = getClosestNode($node, Node\Stmt\Namespace_::class);
/** Map from alias to Definition */
$aliasedDefs = [];
if ($namespace) {
foreach ($namespace->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Use_ || $stmt instanceof Node\Stmt\GroupUse) {
foreach ($stmt->uses as $use) {
// Get the definition for the used namespace, class-like, function or constant
// And save it under the alias
$fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name);
if ($def = $this->index->getDefinition($fqn)) {
$aliasedDefs[$use->alias] = $def;
}
}
} else {
// Use statements are always the first statements in a namespace
break;
}
}
}
// If there is a prefix that does not start with a slash, suggest `use`d symbols
if ($prefix && !$isFullyQualified) {
// Suggest symbols that have been `use`d
// Search the aliases for the typed-in name
foreach ($aliasedDefs as $alias => $def) {
if (substr($alias, 0, $prefixLen) === $prefix) {
unset($prefix);
foreach ($this->index->getDefinitions() as $fqn => $def) {
foreach ($prefixes as $prefix) {
if (substr(strtolower($fqn), 0, strlen($prefix)) === strtolower($prefix) && !$def->isGlobal) {
$list->items[] = CompletionItem::fromDefinition($def);
}
}
}
// Additionally, suggest global symbols that either
// - start with the current namespace + prefix, if the Name node is not fully qualified
// - start with just the prefix, if the Name node is fully qualified
} elseif (ParserHelpers\isConstantFetch($node) ||
($creation = $node->parent) instanceof Node\Expression\ObjectCreationExpression ||
(($creation = $node) instanceof Node\Expression\ObjectCreationExpression)) {
$class = isset($creation) ? $creation->classTypeDesignator : $node;
$prefix = $class instanceof Node\QualifiedName
? (string)PhpParser\ResolvedName::buildName($class->nameParts, $class->getFileContents())
: $class->getText($node->getFileContents());
$namespaceDefinition = $node->getNamespaceDefinition();
list($namespaceImportTable,,) = $node->getImportTablesForCurrentScope();
foreach ($namespaceImportTable as $alias => $name) {
$namespaceImportTable[$alias] = (string)$name;
}
foreach ($this->index->getDefinitions() as $fqn => $def) {
if (
$def->isGlobal // exclude methods, properties etc.
&& (
!$prefix
|| (
((!$namespace || $isFullyQualified) && substr($fqn, 0, $prefixLen) === $prefix)
|| (
$namespace
&& !$isFullyQualified
&& substr($fqn, 0, $namespacedPrefixLen) === $namespacedPrefix
)
)
)
// Only suggest classes for `new`
&& (!($node instanceof Node\Expr\New_) || $def->canBeInstantiated)
) {
$item = CompletionItem::fromDefinition($def);
// Find the shortest name to reference the symbol
if ($namespace && ($alias = array_search($def, $aliasedDefs, true)) !== false) {
// $alias is the name under which this definition is aliased in the current namespace
$item->insertText = $alias;
} else if ($namespace && !($prefix && $isFullyQualified)) {
// Insert the global FQN with trailing backslash
$item->insertText = '\\' . $fqn;
} else {
// Insert the FQN without trailing backlash
$item->insertText = $fqn;
$fqnStartsWithPrefix = substr($fqn, 0, strlen($prefix)) === $prefix;
$fqnContainsPrefix = empty($prefix) || strpos($fqn, $prefix) !== false;
if (($def->canBeInstantiated || ($def->isGlobal && !isset($creation))) && $fqnContainsPrefix) {
if ($namespaceDefinition !== null && $namespaceDefinition->name !== null) {
$namespacePrefix = (string)PhpParser\ResolvedName::buildName($namespaceDefinition->name->nameParts, $node->getFileContents());
$isAliased = false;
$isNotFullyQualified = !($class instanceof Node\QualifiedName) || !$class->isFullyQualifiedName();
if ($isNotFullyQualified) {
foreach ($namespaceImportTable as $alias => $name) {
if (substr($fqn, 0, strlen($name)) === $name) {
$fqn = $alias;
$isAliased = true;
break;
}
}
}
$prefixWithNamespace = $namespacePrefix . "\\" . $prefix;
$fqnMatchesPrefixWithNamespace = substr($fqn, 0, strlen($prefixWithNamespace)) === $prefixWithNamespace;
$isFullyQualifiedAndPrefixMatches = !$isNotFullyQualified && ($fqnStartsWithPrefix || $fqnMatchesPrefixWithNamespace);
if (!$isFullyQualifiedAndPrefixMatches && !$isAliased) {
if (!array_search($fqn, array_values($namespaceImportTable))) {
if (empty($prefix)) {
$fqn = '\\' . $fqn;
} elseif ($fqnMatchesPrefixWithNamespace) {
$fqn = substr($fqn, strlen($namespacePrefix) + 1);
} else {
continue;
}
} else {
continue;
}
}
} elseif ($fqnStartsWithPrefix && $class instanceof Node\QualifiedName && $class->isFullyQualifiedName()) {
$fqn = '\\' . $fqn;
}
$item = CompletionItem::fromDefinition($def);
$item->insertText = $fqn;
$list->items[] = $item;
}
}
// Suggest keywords
if ($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch) {
if (!isset($creation)) {
foreach (self::KEYWORDS as $keyword) {
if (substr($keyword, 0, $prefixLen) === $prefix) {
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
$item->insertText = $keyword . ' ';
$list->items[] = $item;
}
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
$item->insertText = $keyword . ' ';
$list->items[] = $item;
}
}
} else if (
$node instanceof Node\Expr\Variable
|| ($node && $node->getAttribute('parentNode') instanceof Node\Expr\Variable)
) {
// Find variables, parameters and use statements in the scope
// If there was only a $ typed, $node will be instanceof Node\Error
$namePrefix = $node instanceof Node\Expr\Variable && is_string($node->name) ? $node->name : '';
foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) {
$item = new CompletionItem;
$item->kind = CompletionItemKind::VARIABLE;
$item->label = '$' . ($var instanceof Node\Expr\ClosureUse ? $var->var : $var->name);
$item->documentation = $this->definitionResolver->getDocumentationFromNode($var);
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var);
$item->textEdit = new TextEdit(
new Range($pos, $pos),
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), $item->label)
);
} elseif (ParserHelpers\isConstantFetch($node)) {
$prefix = (string) ($node->getResolvedName() ?? PhpParser\ResolvedName::buildName($node->nameParts, $node->getFileContents()));
foreach (self::KEYWORDS as $keyword) {
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
$item->insertText = $keyword . ' ';
$list->items[] = $item;
}
} else if ($node instanceof Node\Stmt\InlineHTML || $pos == new Position(0, 0)) {
$item = new CompletionItem('<?php', CompletionItemKind::KEYWORD);
$item->textEdit = new TextEdit(
new Range($pos, $pos),
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), '<?php')
);
$list->items[] = $item;
}
return $list;
@ -302,7 +324,7 @@ class CompletionProvider
foreach ($fqns as $fqn) {
$def = $this->index->getDefinition($fqn);
if ($def) {
foreach ($this->expandParentFqns($def->extends) as $parent) {
foreach ($this->expandParentFqns($def->extends ?? []) as $parent) {
$expanded[] = $parent;
}
}
@ -335,30 +357,34 @@ class CompletionProvider
// Walk the AST upwards until a scope boundary is met
$level = $node;
while ($level && !($level instanceof Node\FunctionLike)) {
while ($level && !ParserHelpers\isFunctionLike($level)) {
// Walk siblings before the node
$sibling = $level;
while ($sibling = $sibling->getAttribute('previousSibling')) {
while ($sibling = $sibling->getPreviousSibling()) {
// Collect all variables inside the sibling node
foreach ($this->findVariableDefinitionsInNode($sibling, $namePrefix) as $var) {
$vars[$var->name] = $var;
$vars[$var->getName()] = $var;
}
}
$level = $level->getAttribute('parentNode');
$level = $level->parent;
}
// If the traversal ended because a function was met,
// also add its parameters and closure uses to the result list
if ($level instanceof Node\FunctionLike) {
foreach ($level->params as $param) {
if (!isset($vars[$param->name]) && substr($param->name, 0, strlen($namePrefix)) === $namePrefix) {
$vars[$param->name] = $param;
if ($level && ParserHelpers\isFunctionLike($level) && $level->parameters !== null) {
foreach ($level->parameters->getValues() as $param) {
$paramName = $param->getName();
if (empty($namePrefix) || strpos($paramName, $namePrefix) !== false) {
$vars[$paramName] = $param;
}
}
if ($level instanceof Node\Expr\Closure) {
foreach ($level->uses as $use) {
if (!isset($vars[$use->var]) && substr($use->var, 0, strlen($namePrefix)) === $namePrefix) {
$vars[$use->var] = $use;
if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression && $level->anonymousFunctionUseClause !== null &&
$level->anonymousFunctionUseClause->useVariableNameList !== null) {
foreach ($level->anonymousFunctionUseClause->useVariableNameList->getValues() as $use) {
$useName = $use->getName();
if (empty($namePrefix) || strpos($useName, $namePrefix) !== false) {
$vars[$useName] = $use;
}
}
}
@ -372,38 +398,36 @@ class CompletionProvider
*
* @param Node $node
* @param string $namePrefix Prefix to filter
* @return Node\Expr\Variable[]
* @return Node\Expression\Variable[]
*/
private function findVariableDefinitionsInNode(Node $node, string $namePrefix = ''): array
{
$vars = [];
// If the child node is a variable assignment, save it
$parent = $node->getAttribute('parentNode');
if (
$node instanceof Node\Expr\Variable
&& ($parent instanceof Node\Expr\Assign || $parent instanceof Node\Expr\AssignOp)
&& is_string($node->name) // Variable variables are of no use
&& substr($node->name, 0, strlen($namePrefix)) === $namePrefix
) {
$vars[] = $node;
}
// Iterate over subnodes
foreach ($node->getSubNodeNames() as $attr) {
if (!isset($node->$attr)) {
continue;
}
$children = is_array($node->$attr) ? $node->$attr : [$node->$attr];
foreach ($children as $child) {
// Dont try to traverse scalars
// Dont traverse functions, the contained variables are in a different scope
if (!($child instanceof Node) || $child instanceof Node\FunctionLike) {
continue;
}
foreach ($this->findVariableDefinitionsInNode($child, $namePrefix) as $var) {
$vars[] = $var;
$isAssignmentToVariable = function ($node) {
return $node instanceof Node\Expression\AssignmentExpression;
};
if ($this->isAssignmentToVariableWithPrefix($node, $namePrefix)) {
$vars[] = $node->leftOperand;
} else {
// Get all descendent variables, then filter to ones that start with $namePrefix.
// Avoiding closure usage in tight loop
foreach ($node->getDescendantNodes($isAssignmentToVariable) as $descendantNode) {
if ($this->isAssignmentToVariableWithPrefix($descendantNode, $namePrefix)) {
$vars[] = $descendantNode->leftOperand;
}
}
}
return $vars;
}
private function isAssignmentToVariableWithPrefix(Node $node, string $namePrefix): bool
{
return $node instanceof Node\Expression\AssignmentExpression
&& $node->leftOperand instanceof Node\Expression\Variable
&& ($namePrefix === '' || strpos($node->leftOperand->getName(), $namePrefix) !== false);
}
}

View File

@ -10,6 +10,7 @@ use phpDocumentor\Reflection\DocBlockFactory;
use Webmozart\PathUtil\Path;
use Sabre\Uri;
use function Sabre\Event\coroutine;
use Microsoft\PhpParser;
foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
if (file_exists($file)) {
@ -29,7 +30,7 @@ class ComposerScripts
$finder = new FileSystemFilesFinder;
$contentRetriever = new FileSystemContentRetriever;
$docBlockFactory = DocBlockFactory::createInstance();
$parser = new Parser;
$parser = new PhpParser\Parser();
$definitionResolver = new DefinitionResolver($index);
$stubsLocation = null;

View File

@ -3,7 +3,6 @@ declare(strict_types = 1);
namespace LanguageServer;
use PhpParser\Node;
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
use LanguageServer\Protocol\SymbolInformation;
use Exception;

File diff suppressed because it is too large Load Diff

31
src/FqnUtilities.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace LanguageServer\FqnUtilities;
use phpDocumentor\Reflection\{Type, Types};
use Microsoft\PhpParser;
/**
* Returns all possible FQNs in a type
*
* @param Type|null $type
* @return string[]
*/
function getFqnsFromType($type): array
{
$fqns = [];
if ($type instanceof Types\Object_) {
$fqsen = $type->getFqsen();
if ($fqsen !== null) {
$fqns[] = substr((string)$fqsen, 1);
}
}
if ($type instanceof Types\Compound) {
for ($i = 0; $t = $type->get($i); $i++) {
foreach (getFqnsFromType($type) as $fqn) {
$fqns[] = $fqn;
}
}
}
return $fqns;
}

View File

@ -150,6 +150,17 @@ class Index implements ReadableIndex, \Serializable
return $this->references[$fqn] ?? [];
}
/**
* For test use.
* Returns all references, keyed by fqn.
*
* @return string[][]
*/
public function getReferences(): array
{
return $this->references;
}
/**
* Adds a document URI as a referencee of a specific symbol
*

View File

@ -6,6 +6,7 @@ namespace LanguageServer;
use LanguageServer\Cache\Cache;
use LanguageServer\FilesFinder\FilesFinder;
use LanguageServer\Index\{DependenciesIndex, Index};
use LanguageServer\Protocol\Message;
use LanguageServer\Protocol\MessageType;
use Webmozart\PathUtil\Path;
use Composer\Semver\VersionParser;

View File

@ -8,10 +8,7 @@ use LanguageServer\Protocol\{
ClientCapabilities,
TextDocumentSyncKind,
Message,
MessageType,
InitializeResult,
SymbolInformation,
TextDocumentIdentifier,
CompletionOptions
};
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
@ -19,12 +16,10 @@ use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, F
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
use LanguageServer\Cache\{FileSystemCache, ClientCache};
use AdvancedJsonRpc;
use Sabre\Event\{Loop, Promise};
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
use Exception;
use Throwable;
use Webmozart\PathUtil\Path;
use Sabre\Uri;
class LanguageServer extends AdvancedJsonRpc\Dispatcher
{

View File

@ -1,41 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
class ColumnCalculator extends NodeVisitorAbstract
{
private $code;
private $codeLength;
public function __construct($code)
{
$this->code = $code;
$this->codeLength = strlen($code);
}
public function enterNode(Node $node)
{
$startFilePos = $node->getAttribute('startFilePos');
$endFilePos = $node->getAttribute('endFilePos');
if ($startFilePos > $this->codeLength || $endFilePos > $this->codeLength) {
throw new \RuntimeException('Invalid position information');
}
$startLinePos = strrpos($this->code, "\n", $startFilePos - $this->codeLength);
if ($startLinePos === false) {
$startLinePos = -1;
}
$endLinePos = strrpos($this->code, "\n", $endFilePos - $this->codeLength);
if ($endLinePos === false) {
$endLinePos = -1;
}
$node->setAttribute('startColumn', $startFilePos - $startLinePos);
$node->setAttribute('endColumn', $endFilePos - $endLinePos);
}
}

View File

@ -1,47 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
use LanguageServer\{Definition, DefinitionResolver};
use LanguageServer\Protocol\SymbolInformation;
/**
* Collects definitions of classes, interfaces, traits, methods, properties and constants
* Depends on ReferencesAdder and NameResolver
*/
class DefinitionCollector extends NodeVisitorAbstract
{
/**
* Map from fully qualified name (FQN) to Definition
*
* @var Definition[]
*/
public $definitions = [];
/**
* Map from fully qualified name (FQN) to Node
*
* @var Node[]
*/
public $nodes = [];
private $definitionResolver;
public function __construct(DefinitionResolver $definitionResolver)
{
$this->definitionResolver = $definitionResolver;
}
public function enterNode(Node $node)
{
$fqn = DefinitionResolver::getDefinedFqn($node);
// Only index definitions with an FQN (no variables)
if ($fqn === null) {
return;
}
$this->nodes[$fqn] = $node;
$this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn);
}
}

View File

@ -1,96 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser;
use PhpParser\{NodeVisitorAbstract, Node, Comment};
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\Types\Context;
use Exception;
/**
* Decorates all nodes with a docBlock attribute that is an instance of phpDocumentor\Reflection\DocBlock
*/
class DocBlockParser extends NodeVisitorAbstract
{
/**
* @var DocBlockFactory
*/
private $docBlockFactory;
/**
* The current namespace context
*
* @var string
*/
private $namespace;
/**
* Prefix from a parent group use declaration
*
* @var string
*/
private $prefix;
/**
* Namespace aliases in the current context
*
* @var string[]
*/
private $aliases;
/**
* @var PhpParser\Error[]
*/
public $errors = [];
public function __construct(DocBlockFactory $docBlockFactory)
{
$this->docBlockFactory = $docBlockFactory;
}
public function beforeTraverse(array $nodes)
{
$this->namespace = '';
$this->prefix = '';
$this->aliases = [];
}
public function enterNode(Node $node)
{
if ($node instanceof Node\Stmt\Namespace_) {
$this->namespace = (string)$node->name;
} else if ($node instanceof Node\Stmt\GroupUse) {
$this->prefix = (string)$node->prefix . '\\';
} else if ($node instanceof Node\Stmt\UseUse) {
$this->aliases[$node->alias] = $this->prefix . (string)$node->name;
}
$docComment = $node->getDocComment();
if ($docComment === null) {
return;
}
$context = new Context($this->namespace, $this->aliases);
try {
$docBlock = $this->docBlockFactory->create($docComment->getText(), $context);
$node->setAttribute('docBlock', $docBlock);
} catch (Exception $e) {
$this->errors[] = new PhpParser\Error($e->getMessage(), [
'startFilePos' => $docComment->getFilePos(),
'endFilePos' => $docComment->getFilePos() + strlen($docComment->getText()),
'startLine' => $docComment->getLine(),
'endLine' => $docComment->getLine() + preg_match_all('/[\\n\\r]/', $docComment->getText()) + 1
]);
}
}
public function leaveNode(Node $node)
{
if ($node instanceof Node\Stmt\Namespace_) {
$this->namespace = '';
$this->aliases = [];
} else if ($node instanceof Node\Stmt\GroupUse) {
$this->prefix = '';
}
}
}

View File

@ -1,45 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node, NodeTraverser};
use LanguageServer\Protocol\{Position, Range};
/**
* Finds the Node at a specified position
* Depends on ColumnCalculator
*/
class NodeAtPositionFinder extends NodeVisitorAbstract
{
/**
* The node at the position, if found
*
* @var Node|null
*/
public $node;
/**
* @var Position
*/
private $position;
/**
* @param Position $position The position where the node is located
*/
public function __construct(Position $position)
{
$this->position = $position;
}
public function leaveNode(Node $node)
{
if ($this->node === null) {
$range = Range::fromNode($node);
if ($range->includes($this->position)) {
$this->node = $node;
return NodeTraverser::STOP_TRAVERSAL;
}
}
}
}

View File

@ -1,54 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
/**
* Decorates all nodes with parent and sibling references (similar to DOM nodes)
*/
class ReferencesAdder extends NodeVisitorAbstract
{
/**
* @var Node[]
*/
private $stack = [];
/**
* @var Node
*/
private $previous;
/**
* @var mixed
*/
private $document;
/**
* @param mixed $document The value for the ownerDocument attribute
*/
public function __construct($document = null)
{
$this->document = $document;
}
public function enterNode(Node $node)
{
$node->setAttribute('ownerDocument', $this->document);
if (!empty($this->stack)) {
$node->setAttribute('parentNode', end($this->stack));
}
if (isset($this->previous) && $this->previous->getAttribute('parentNode') === $node->getAttribute('parentNode')) {
$node->setAttribute('previousSibling', $this->previous);
$this->previous->setAttribute('nextSibling', $node);
}
$this->stack[] = $node;
}
public function leaveNode(Node $node)
{
$this->previous = $node;
array_pop($this->stack);
}
}

View File

@ -1,75 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
use LanguageServer\DefinitionResolver;
/**
* Collects references to classes, interfaces, traits, methods, properties and constants
* Depends on ReferencesAdder and NameResolver
*/
class ReferencesCollector extends NodeVisitorAbstract
{
/**
* Map from fully qualified name (FQN) to array of nodes that reference the symbol
*
* @var Node[][]
*/
public $nodes = [];
/**
* @var DefinitionResolver
*/
private $definitionResolver;
/**
* @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve reference nodes to definitions
*/
public function __construct(DefinitionResolver $definitionResolver)
{
$this->definitionResolver = $definitionResolver;
}
public function enterNode(Node $node)
{
// Check if the node references any global symbol
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
if ($fqn) {
$parent = $node->getAttribute('parentNode');
$grandParent = $parent ? $parent->getAttribute('parentNode') : null;
$this->addReference($fqn, $node);
if (
$node instanceof Node\Name
&& $node->isQualified()
&& !($parent instanceof Node\Stmt\Namespace_ && $parent->name === $node)
) {
// 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 ($parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall) {
$parts = explode('\\', $fqn);
if (count($parts) > 1) {
$globalFqn = end($parts);
$this->addReference($globalFqn, $node);
}
}
}
}
private function addReference(string $fqn, Node $node)
{
if (!isset($this->nodes[$fqn])) {
$this->nodes[$fqn] = [];
}
$this->nodes[$fqn][] = $node;
}
}

View File

@ -1,50 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node, NodeTraverser};
/**
* Collects all references to a variable
*/
class VariableReferencesCollector extends NodeVisitorAbstract
{
/**
* Array of references to the variable
*
* @var Node\Expr\Variable[]
*/
public $nodes = [];
/**
* @var string
*/
private $name;
/**
* @param string $name The variable name
*/
public function __construct(string $name)
{
$this->name = $name;
}
public function enterNode(Node $node)
{
if ($node instanceof Node\Expr\Variable && $node->name === $this->name) {
$this->nodes[] = $node;
} else if ($node instanceof Node\FunctionLike) {
// If we meet a function node, dont traverse its statements, they are in another scope
// except it is a closure that has imported the variable through use
if ($node instanceof Node\Expr\Closure) {
foreach ($node->uses as $use) {
if ($use->var === $this->name) {
return;
}
}
}
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
}
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace LanguageServer;
use PhpParser;
/**
* Custom PHP Parser class configured for our needs
*/
class Parser extends PhpParser\Parser\Php7
{
public function __construct()
{
$lexer = new PhpParser\Lexer([
'usedAttributes' => [
'comments',
'startLine',
'endLine',
'startFilePos',
'endFilePos'
]
]);
parent::__construct($lexer);
}
}

124
src/ParserHelpers.php Normal file
View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace LanguageServer\ParserHelpers;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
function isConstantFetch(Node $node) : bool
{
$parent = $node->parent;
return
(
$node instanceof Node\QualifiedName &&
(
$parent instanceof Node\Expression ||
$parent instanceof Node\DelimitedList\ExpressionList ||
$parent instanceof Node\ArrayElement ||
($parent instanceof Node\Parameter && $node->parent->default === $node) ||
$parent instanceof Node\StatementNode ||
$parent instanceof Node\CaseStatementNode
) &&
!(
$parent instanceof Node\Expression\MemberAccessExpression ||
$parent instanceof Node\Expression\CallExpression ||
$parent instanceof Node\Expression\ObjectCreationExpression ||
$parent instanceof Node\Expression\ScopedPropertyAccessExpression ||
isFunctionLike($parent) ||
(
$parent instanceof Node\Expression\BinaryExpression &&
$parent->operator->kind === PhpParser\TokenKind::InstanceOfKeyword
)
));
}
function getFunctionLikeDeclarationFromParameter(Node\Parameter $node)
{
return $node->parent->parent;
}
function isFunctionLike(Node $node)
{
return
$node instanceof Node\Statement\FunctionDeclaration ||
$node instanceof Node\MethodDeclaration ||
$node instanceof Node\Expression\AnonymousFunctionCreationExpression;
}
function isBooleanExpression($expression) : bool
{
if (!($expression instanceof Node\Expression\BinaryExpression)) {
return false;
}
switch ($expression->operator->kind) {
case PhpParser\TokenKind::InstanceOfKeyword:
case PhpParser\TokenKind::GreaterThanToken:
case PhpParser\TokenKind::GreaterThanEqualsToken:
case PhpParser\TokenKind::LessThanToken:
case PhpParser\TokenKind::LessThanEqualsToken:
case PhpParser\TokenKind::AndKeyword:
case PhpParser\TokenKind::AmpersandAmpersandToken:
case PhpParser\TokenKind::LessThanEqualsGreaterThanToken:
case PhpParser\TokenKind::OrKeyword:
case PhpParser\TokenKind::BarBarToken:
case PhpParser\TokenKind::XorKeyword:
case PhpParser\TokenKind::ExclamationEqualsEqualsToken:
case PhpParser\TokenKind::ExclamationEqualsToken:
case PhpParser\TokenKind::CaretToken:
case PhpParser\TokenKind::EqualsEqualsEqualsToken:
case PhpParser\TokenKind::EqualsToken:
return true;
}
return false;
}
/**
* Tries to get the parent property declaration given a Node
* @param Node $node
* @return Node\PropertyDeclaration | null $node
*/
function tryGetPropertyDeclaration(Node $node)
{
if ($node instanceof Node\Expression\Variable &&
(($propertyDeclaration = $node->parent->parent) instanceof Node\PropertyDeclaration ||
($propertyDeclaration = $propertyDeclaration->parent) instanceof Node\PropertyDeclaration)
) {
return $propertyDeclaration;
}
return null;
}
/**
* Tries to get the parent ConstDeclaration or ClassConstDeclaration given a Node
* @param Node $node
* @return Node\Statement\ConstDeclaration | Node\ClassConstDeclaration | null $node
*/
function tryGetConstOrClassConstDeclaration(Node $node)
{
if (
$node instanceof Node\ConstElement && (
($constDeclaration = $node->parent->parent) instanceof Node\ClassConstDeclaration ||
$constDeclaration instanceof Node\Statement\ConstDeclaration )
) {
return $constDeclaration;
}
return null;
}
/**
* Returns true if the node is a usage of `define`.
* e.g. define('TEST_DEFINE_CONSTANT', false);
* @param Node $node
* @return bool
*/
function isConstDefineExpression(Node $node): bool
{
return $node instanceof Node\Expression\CallExpression
&& $node->callableExpression instanceof Node\QualifiedName
&& strtolower($node->callableExpression->getText()) === 'define'
&& isset($node->argumentExpressionList->children[0])
&& $node->argumentExpressionList->children[0]->expression instanceof Node\StringLiteral
&& isset($node->argumentExpressionList->children[2]);
}

View File

@ -3,27 +3,20 @@ declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit};
use LanguageServer\NodeVisitor\{
NodeAtPositionFinder,
ReferencesAdder,
DocBlockParser,
DefinitionCollector,
ColumnCalculator,
ReferencesCollector
};
use LanguageServer\Index\Index;
use PhpParser\{Error, ErrorHandler, Node, NodeTraverser};
use PhpParser\NodeVisitor\NameResolver;
use LanguageServer\Protocol\{
Diagnostic, Position, Range
};
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Uri;
class PhpDocument
{
/**
* The PHPParser instance
*
* @var Parser
* @var PhpParser\Parser
*/
private $parser;
@ -63,7 +56,7 @@ class PhpDocument
/**
* The AST of the document
*
* @var Node[]
* @var Node
*/
private $stmts;
@ -77,7 +70,7 @@ class PhpDocument
/**
* Map from fully qualified name (FQN) to Node
*
* @var Node[]
* @var Node
*/
private $definitionNodes;
@ -96,18 +89,18 @@ class PhpDocument
private $diagnostics;
/**
* @param string $uri The URI of the document
* @param string $content The content of the document
* @param Index $index The Index to register definitions and references to
* @param Parser $parser The PHPParser instance
* @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks
* @param string $uri The URI of the document
* @param string $content The content of the document
* @param Index $index The Index to register definitions and references to
* @param PhpParser\Parser $parser The PhpParser instance
* @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks
* @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve definitions to symbols in the workspace
*/
public function __construct(
string $uri,
string $content,
Index $index,
Parser $parser,
$parser,
DocBlockFactory $docBlockFactory,
DefinitionResolver $definitionResolver
) {
@ -133,7 +126,7 @@ class PhpDocument
/**
* Updates the content on this document.
* Re-parses a source file, updates symbols and reports parsing errors
* that may have occured as diagnostics.
* that may have occurred as diagnostics.
*
* @param string $content
* @return void
@ -160,64 +153,26 @@ class PhpDocument
$this->definitions = null;
$this->definitionNodes = null;
$errorHandler = new ErrorHandler\Collecting;
$stmts = $this->parser->parse($content, $errorHandler);
$treeAnalyzer = new TreeAnalyzer($this->parser, $content, $this->docBlockFactory, $this->definitionResolver, $this->uri);
$this->diagnostics = [];
foreach ($errorHandler->getErrors() as $error) {
$this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php');
$this->diagnostics = $treeAnalyzer->getDiagnostics();
$this->definitions = $treeAnalyzer->getDefinitions();
$this->definitionNodes = $treeAnalyzer->getDefinitionNodes();
$this->referenceNodes = $treeAnalyzer->getReferenceNodes();
foreach ($this->definitions as $fqn => $definition) {
$this->index->setDefinition($fqn, $definition);
}
// $stmts can be null in case of a fatal parsing error
if ($stmts) {
$traverser = new NodeTraverser;
// Resolve aliased names to FQNs
$traverser->addVisitor(new NameResolver($errorHandler));
// Add parentNode, previousSibling, nextSibling attributes
$traverser->addVisitor(new ReferencesAdder($this));
// Add column attributes to nodes
$traverser->addVisitor(new ColumnCalculator($content));
// Parse docblocks and add docBlock attributes to nodes
$docBlockParser = new DocBlockParser($this->docBlockFactory);
$traverser->addVisitor($docBlockParser);
$traverser->traverse($stmts);
// Report errors from parsing docblocks
foreach ($docBlockParser->errors as $error) {
$this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php');
}
$traverser = new NodeTraverser;
// Collect all definitions
$definitionCollector = new DefinitionCollector($this->definitionResolver);
$traverser->addVisitor($definitionCollector);
// Collect all references
$referencesCollector = new ReferencesCollector($this->definitionResolver);
$traverser->addVisitor($referencesCollector);
$traverser->traverse($stmts);
// Register this document on the project for all the symbols defined in it
$this->definitions = $definitionCollector->definitions;
$this->definitionNodes = $definitionCollector->nodes;
foreach ($definitionCollector->definitions as $fqn => $definition) {
$this->index->setDefinition($fqn, $definition);
}
// Register this document on the project for references
$this->referenceNodes = $referencesCollector->nodes;
foreach ($referencesCollector->nodes as $fqn => $nodes) {
$this->index->addReferenceUri($fqn, $this->uri);
}
$this->stmts = $stmts;
// Register this document on the project for references
foreach ($this->referenceNodes as $fqn => $nodes) {
$this->index->addReferenceUri($fqn, $this->uri);
}
$this->stmts = $treeAnalyzer->getStmts();
}
/**
@ -266,9 +221,9 @@ class PhpDocument
/**
* Returns the AST of the document
*
* @return Node[]
* @return Node | null
*/
public function getStmts(): array
public function getStmts()
{
return $this->stmts;
}
@ -284,11 +239,13 @@ class PhpDocument
if ($this->stmts === null) {
return null;
}
$traverser = new NodeTraverser;
$finder = new NodeAtPositionFinder($position);
$traverser->addVisitor($finder);
$traverser->traverse($this->stmts);
return $finder->node;
$offset = $position->toOffset($this->stmts->getFileContents());
$node = $this->stmts->getDescendantNodeAtPosition($offset);
if ($node !== null && $node->getStart() > $offset) {
return null;
}
return $node;
}
/**

View File

@ -8,6 +8,7 @@ use LanguageServer\Index\ProjectIndex;
use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
use Microsoft\PhpParser;
/**
* Takes care of loading documents and managing "open" documents
@ -36,6 +37,11 @@ class PhpDocumentLoader
*/
private $parser;
/**
* @var PhpParser\Parser
*/
private $tolerantParser;
/**
* @var DocBlockFactory
*/
@ -47,9 +53,10 @@ class PhpDocumentLoader
private $definitionResolver;
/**
* @param ContentRetriever $contentRetriever
* @param ProjectIndex $project
* @param ContentRetriever $contentRetriever
* @param ProjectIndex $projectIndex
* @param DefinitionResolver $definitionResolver
* @internal param ProjectIndex $project
*/
public function __construct(
ContentRetriever $contentRetriever,
@ -59,7 +66,7 @@ class PhpDocumentLoader
$this->contentRetriever = $contentRetriever;
$this->projectIndex = $projectIndex;
$this->definitionResolver = $definitionResolver;
$this->parser = new Parser;
$this->parser = new PhpParser\Parser();
$this->docBlockFactory = DocBlockFactory::createInstance();
}

View File

@ -2,8 +2,6 @@
namespace LanguageServer\Protocol;
use PhpParser\Error;
/**
* Represents a diagnostic, such as a compiler error or warning. Diagnostic objects are only valid in the scope of a
* resource.
@ -47,26 +45,6 @@ class Diagnostic
*/
public $message;
/**
* Creates a diagnostic from a PhpParser Error
*
* @param Error $error Message and code will be used
* @param string $content The file content to calculate the column info
* @param int $severity DiagnosticSeverity
* @param string $source A human-readable string describing the source of this diagnostic
* @return self
*/
public static function fromError(Error $error, string $content, int $severity = null, string $source = null): self
{
return new self(
$error->getRawMessage(), // Do not include "on line ..." in the error message
Range::fromError($error, $content),
$error->getCode(),
$severity,
$source
);
}
/**
* @param string $message The diagnostic's message
* @param Range $range The range at which the message applies

View File

@ -2,7 +2,8 @@
namespace LanguageServer\Protocol;
use PhpParser\Node;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
/**
* Represents a location inside a resource, such as a line inside a text file.
@ -25,9 +26,13 @@ class Location
* @param Node $node
* @return self
*/
public static function fromNode(Node $node)
public static function fromNode($node)
{
return new self($node->getAttribute('ownerDocument')->getUri(), Range::fromNode($node));
$range = PhpParser\PositionUtilities::getRangeFromPosition($node->getStart(), $node->getWidth(), $node->getFileContents());
return new self($node->getUri(), new Range(
new Position($range->start->line, $range->start->character),
new Position($range->end->line, $range->end->character)
));
}
public function __construct(string $uri = null, Range $range = null)

View File

@ -2,7 +2,8 @@
namespace LanguageServer\Protocol;
use PhpParser\{Error, Node};
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
/**
* A range in a text document expressed as (zero-based) start and end positions.
@ -31,26 +32,12 @@ class Range
*/
public static function fromNode(Node $node)
{
return new self(
new Position($node->getAttribute('startLine') - 1, $node->getAttribute('startColumn') - 1),
new Position($node->getAttribute('endLine') - 1, $node->getAttribute('endColumn'))
);
}
$range = PhpParser\PositionUtilities::getRangeFromPosition($node->getStart(), $node->getWidth(), $node->getFileContents());
/**
* Returns the range where an error occured
*
* @param \PhpParser\Error $error
* @param string $content
* @return self
*/
public static function fromError(Error $error, string $content)
{
$startLine = max($error->getStartLine() - 1, 0);
$endLine = max($error->getEndLine() - 1, $startLine);
$startColumn = $error->hasColumnInfo() ? $error->getStartColumn($content) - 1 : 0;
$endColumn = $error->hasColumnInfo() ? $error->getEndColumn($content) : 0;
return new self(new Position($startLine, $startColumn), new Position($endLine, $endColumn));
return new self(
new Position($range->start->line, $range->start->character),
new Position($range->end->line, $range->end->character)
);
}
public function __construct(Position $start = null, Position $end = null)

View File

@ -2,7 +2,8 @@
namespace LanguageServer\Protocol;
use PhpParser\Node;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use Exception;
/**
@ -44,66 +45,68 @@ class SymbolInformation
*
* @param Node $node
* @param string $fqn If given, $containerName will be extracted from it
* @return self|null
* @return SymbolInformation|null
*/
public static function fromNode(Node $node, string $fqn = null)
public static function fromNode($node, string $fqn = null)
{
$parent = $node->getAttribute('parentNode');
$symbol = new self;
if (
$node instanceof Node\Expr\FuncCall
&& $node->name instanceof Node\Name
&& strtolower((string)$node->name) === 'define'
&& isset($node->args[0])
&& $node->args[0]->value instanceof Node\Scalar\String_
&& isset($node->args[1])
) {
if ($node instanceof Node\Statement\ClassDeclaration) {
$symbol->kind = SymbolKind::CLASS_;
} else if ($node instanceof Node\Statement\TraitDeclaration) {
$symbol->kind = SymbolKind::CLASS_;
} else if (\LanguageServer\ParserHelpers\isConstDefineExpression($node)) {
// constants with define() like
// define('TEST_DEFINE_CONSTANT', false);
$symbol->kind = SymbolKind::CONSTANT;
$symbol->name = (string)$node->args[0]->value->value;
} else if ($node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Trait_) {
$symbol->kind = SymbolKind::CLASS_;
} else if ($node instanceof Node\Stmt\Interface_) {
$symbol->name = $node->argumentExpressionList->children[0]->expression->getStringContentsText();
} else if ($node instanceof Node\Statement\InterfaceDeclaration) {
$symbol->kind = SymbolKind::INTERFACE;
} else if ($node instanceof Node\Name && $parent instanceof Node\Stmt\Namespace_) {
} else if ($node instanceof Node\Statement\NamespaceDefinition) {
$symbol->kind = SymbolKind::NAMESPACE;
} else if ($node instanceof Node\Stmt\Function_) {
} else if ($node instanceof Node\Statement\FunctionDeclaration) {
$symbol->kind = SymbolKind::FUNCTION;
} else if ($node instanceof Node\Stmt\ClassMethod && ($node->name === '__construct' || $node->name === '__destruct')) {
$symbol->kind = SymbolKind::CONSTRUCTOR;
} else if ($node instanceof Node\Stmt\ClassMethod) {
$symbol->kind = SymbolKind::METHOD;
} else if ($node instanceof Node\Stmt\PropertyProperty) {
} else if ($node instanceof Node\MethodDeclaration) {
$nameText = $node->getName();
if ($nameText === '__construct' || $nameText === '__destruct') {
$symbol->kind = SymbolKind::CONSTRUCTOR;
} else {
$symbol->kind = SymbolKind::METHOD;
}
} else if ($node instanceof Node\Expression\Variable && $node->getFirstAncestor(Node\PropertyDeclaration::class) !== null) {
$symbol->kind = SymbolKind::PROPERTY;
} else if ($node instanceof Node\Const_) {
} else if ($node instanceof Node\ConstElement) {
$symbol->kind = SymbolKind::CONSTANT;
} else if (
(
($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp)
&& $node->var instanceof Node\Expr\Variable
($node instanceof Node\Expression\AssignmentExpression)
&& $node->leftOperand instanceof Node\Expression\Variable
)
|| $node instanceof Node\Expr\ClosureUse
|| $node instanceof Node\Param
|| $node instanceof Node\UseVariableName
|| $node instanceof Node\Parameter
) {
$symbol->kind = SymbolKind::VARIABLE;
} else {
return null;
}
if (!isset($symbol->name)) {
if ($node instanceof Node\Name) {
$symbol->name = (string)$node;
} else if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) {
$symbol->name = $node->var->name;
} else if ($node instanceof Node\Expr\ClosureUse) {
$symbol->name = $node->var;
} else if (isset($node->name)) {
$symbol->name = (string)$node->name;
} else {
return null;
if ($node instanceof Node\Expression\AssignmentExpression) {
if ($node->leftOperand instanceof Node\Expression\Variable) {
$symbol->name = $node->leftOperand->getName();
} elseif ($node->leftOperand instanceof PhpParser\Token) {
$symbol->name = trim($node->leftOperand->getText($node->getFileContents()), "$");
}
} else if ($node instanceof Node\UseVariableName) {
$symbol->name = $node->getName();
} else if (isset($node->name)) {
if ($node->name instanceof Node\QualifiedName) {
$symbol->name = (string)PhpParser\ResolvedName::buildName($node->name->nameParts, $node->getFileContents());
} else {
$symbol->name = ltrim((string)$node->name->getText($node->getFileContents()), "$");
}
} else if (isset($node->variableName)) {
$symbol->name = $node->variableName->getText($node);
} else if (!isset($symbol->name)) {
return null;
}
$symbol->location = Location::fromNode($node);

View File

@ -3,34 +3,21 @@ declare(strict_types = 1);
namespace LanguageServer\Server;
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use PhpParser\{Node, NodeTraverser};
use LanguageServer\{LanguageClient, PhpDocumentLoader, PhpDocument, DefinitionResolver, CompletionProvider};
use LanguageServer\NodeVisitor\VariableReferencesCollector;
use LanguageServer\Protocol\{
SymbolLocationInformation,
SymbolDescriptor,
TextDocumentItem,
TextDocumentIdentifier,
VersionedTextDocumentIdentifier,
Position,
Range,
FormattingOptions,
TextEdit,
Location,
SymbolInformation,
ReferenceContext,
Hover,
MarkedString,
SymbolKind,
CompletionItem,
CompletionItemKind
use LanguageServer\{
CompletionProvider, LanguageClient, PhpDocument, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{
FormattingOptions, Hover, Location, MarkedString, Position, Range, ReferenceContext, SymbolDescriptor, SymbolLocationInformation, TextDocumentIdentifier, TextDocumentItem, VersionedTextDocumentIdentifier
};
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use Sabre\Event\Promise;
use Sabre\Uri;
use function LanguageServer\{
isVendored, waitForEvent
};
use function Sabre\Event\coroutine;
use function LanguageServer\{waitForEvent, isVendored};
/**
* Provides method handlers for all textDocument/* methods
@ -49,11 +36,6 @@ class TextDocument
*/
protected $project;
/**
* @var PrettyPrinter
*/
protected $prettyPrinter;
/**
* @var DefinitionResolver
*/
@ -80,12 +62,12 @@ class TextDocument
protected $composerLock;
/**
* @param PhpDocumentLoader $documentLoader
* @param PhpDocumentLoader $documentLoader
* @param DefinitionResolver $definitionResolver
* @param LanguageClient $client
* @param ReadableIndex $index
* @param \stdClass $composerJson
* @param \stdClass $composerLock
* @param LanguageClient $client
* @param ReadableIndex $index
* @param \stdClass $composerJson
* @param \stdClass $composerLock
*/
public function __construct(
PhpDocumentLoader $documentLoader,
@ -97,7 +79,6 @@ class TextDocument
) {
$this->documentLoader = $documentLoader;
$this->client = $client;
$this->prettyPrinter = new PrettyPrinter();
$this->definitionResolver = $definitionResolver;
$this->completionProvider = new CompletionProvider($this->definitionResolver, $index);
$this->index = $index;
@ -202,31 +183,34 @@ class TextDocument
// Variables always stay in the boundary of the file and need to be searched inside their function scope
// by traversing the AST
if (
$node instanceof Node\Expr\Variable
|| $node instanceof Node\Param
|| $node instanceof Node\Expr\ClosureUse
($node instanceof Node\Expression\Variable && !($node->getParent()->getParent() instanceof Node\PropertyDeclaration))
|| $node instanceof Node\Parameter
|| $node instanceof Node\UseVariableName
) {
if ($node->name instanceof Node\Expr) {
if (isset($node->name) && $node->name instanceof Node\Expression) {
return null;
}
// Find function/method/closure scope
$n = $node;
while (isset($n) && !($n instanceof Node\FunctionLike)) {
$n = $n->getAttribute('parentNode');
$n = $n->getFirstAncestor(Node\Statement\FunctionDeclaration::class, Node\MethodDeclaration::class, Node\Expression\AnonymousFunctionCreationExpression::class, Node\SourceFileNode::class);
if ($n === null) {
$n = $node->getFirstAncestor(Node\Statement\ExpressionStatement::class)->getParent();
}
if (!isset($n)) {
$n = $node->getAttribute('ownerDocument');
}
$traverser = new NodeTraverser;
$refCollector = new VariableReferencesCollector($node->name);
$traverser->addVisitor($refCollector);
$traverser->traverse($n->getStmts());
foreach ($refCollector->nodes as $ref) {
$locations[] = Location::fromNode($ref);
foreach ($n->getDescendantNodes() as $descendantNode) {
if ($descendantNode instanceof Node\Expression\Variable &&
$descendantNode->getName() === $node->getName()
) {
$locations[] = Location::fromNode($descendantNode);
}
}
} else {
// Definition with a global FQN
$fqn = DefinitionResolver::getDefinedFqn($node);
// Wait until indexing finished
if (!$this->index->isComplete()) {
yield waitForEvent($this->index, 'complete');

209
src/TreeAnalyzer.php Normal file
View File

@ -0,0 +1,209 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit};
use LanguageServer\Index\Index;
use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Uri;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
class TreeAnalyzer
{
/** @var PhpParser\Parser */
private $parser;
/** @var Node */
private $stmts;
/** @var Diagnostic[] */
private $diagnostics;
/** @var string */
private $content;
/** @var Node[] */
private $referenceNodes;
/** @var Definition[] */
private $definitions;
/** @var Node[] */
private $definitionNodes;
/**
* @param PhpParser\Parser $parser
* @param string $content
* @param DocBlockFactory $docBlockFactory
* @param DefinitionResolver $definitionResolver
* @param string $uri
*/
public function __construct(PhpParser\Parser $parser, string $content, DocBlockFactory $docBlockFactory, DefinitionResolver $definitionResolver, string $uri)
{
$this->parser = $parser;
$this->docBlockFactory = $docBlockFactory;
$this->definitionResolver = $definitionResolver;
$this->content = $content;
$this->stmts = $this->parser->parseSourceFile($content, $uri);
// TODO - docblock errors
$this->collectDefinitionsAndReferences($this->stmts);
}
private function collectDefinitionsAndReferences(Node $stmts)
{
foreach ($stmts::CHILD_NAMES as $name) {
$node = $stmts->$name;
if ($node === null) {
continue;
}
if (\is_array($node)) {
foreach ($node as $child) {
if ($child instanceof Node) {
$this->update($child);
}
}
continue;
}
if ($node instanceof Node) {
$this->update($node);
}
if (($error = PhpParser\DiagnosticsProvider::checkDiagnostics($node)) !== null) {
$range = PhpParser\PositionUtilities::getRangeFromPosition($error->start, $error->length, $this->content);
$this->diagnostics[] = new Diagnostic(
$error->message,
new Range(
new Position($range->start->line, $range->start->character),
new Position($range->end->line, $range->start->character)
),
null,
DiagnosticSeverity::ERROR,
'php'
);
}
}
}
/**
* Collect definitions and references for the given node
*
* @param Node $node
*/
private function update(Node $node)
{
$fqn = ($this->definitionResolver)::getDefinedFqn($node);
// Only index definitions with an FQN (no variables)
if ($fqn !== null) {
$this->definitionNodes[$fqn] = $node;
$this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn);
} else {
$parent = $node->parent;
if (!(
(
// $node->parent instanceof Node\Expression\ScopedPropertyAccessExpression ||
($node instanceof Node\Expression\ScopedPropertyAccessExpression ||
$node instanceof Node\Expression\MemberAccessExpression)
&& !(
$node->parent instanceof Node\Expression\CallExpression ||
$node->memberName instanceof PhpParser\Token
))
|| ($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);
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);
}
}
}
}
}
$this->collectDefinitionsAndReferences($node);
}
/**
* @return Diagnostic[]
*/
public function getDiagnostics(): array
{
return $this->diagnostics ?? [];
}
/**
* @return void
*/
private function addReference(string $fqn, Node $node)
{
if (!isset($this->referenceNodes[$fqn])) {
$this->referenceNodes[$fqn] = [];
}
$this->referenceNodes[$fqn][] = $node;
}
/**
* @return Definition
*/
public function getDefinitions()
{
return $this->definitions ?? [];
}
/**
* @return Node[]
*/
public function getDefinitionNodes()
{
return $this->definitionNodes ?? [];
}
/**
* @return Node[]
*/
public function getReferenceNodes()
{
return $this->referenceNodes ?? [];
}
/**
* @return Node[]
*/
public function getStmts()
{
return $this->stmts;
}
}

View File

@ -5,7 +5,6 @@ namespace LanguageServer;
use Throwable;
use InvalidArgumentException;
use PhpParser\Node;
use Sabre\Event\{Loop, Promise, EmitterInterface};
use Sabre\Uri;
@ -94,23 +93,6 @@ function waitForEvent(EmitterInterface $emitter, string $event): Promise
return $p;
}
/**
* Returns the closest node of a specific type
*
* @param Node $node
* @param string $type The node class name
* @return Node|null $type
*/
function getClosestNode(Node $node, string $type)
{
$n = $node;
while ($n = $n->getAttribute('parentNode')) {
if ($n instanceof $type) {
return $n;
}
}
}
/**
* Returns the part of $b that is not overlapped by $a
* Example:

View File

@ -5,32 +5,33 @@ namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase;
use LanguageServer\Index\Index;
use LanguageServer\{DefinitionResolver, Parser};
use LanguageServer\DefinitionResolver;
use Microsoft\PhpParser;
class DefinitionResolverTest extends TestCase
{
public function testCreateDefinitionFromNode()
{
$parser = new Parser;
$stmts = $parser->parse("<?php\ndefine('TEST_DEFINE', true);");
$stmts[0]->setAttribute('ownerDocument', new MockPhpDocument);
$parser = new PhpParser\Parser;
$doc = new MockPhpDocument;
$stmts = $parser->parseSourceFile("<?php\ndefine('TEST_DEFINE', true);", $doc->getUri());
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$def = $definitionResolver->createDefinitionFromNode($stmts[0], '\TEST_DEFINE');
$def = $definitionResolver->createDefinitionFromNode($stmts->statementList[1]->expression, '\TEST_DEFINE');
$this->assertInstanceOf(\phpDocumentor\Reflection\Types\Boolean::class, $def->type);
}
public function testGetTypeFromNode()
{
$parser = new Parser;
$stmts = $parser->parse("<?php\ndefine('TEST_DEFINE', true);");
$stmts[0]->setAttribute('ownerDocument', new MockPhpDocument);
$parser = new PhpParser\Parser;
$doc = new MockPhpDocument;
$stmts = $parser->parseSourceFile("<?php\ndefine('TEST_DEFINE', true);", $doc->getUri());
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$type = $definitionResolver->getTypeFromNode($stmts[0]);
$type = $definitionResolver->getTypeFromNode($stmts->statementList[1]->expression);
$this->assertInstanceOf(\phpDocumentor\Reflection\Types\Boolean::class, $type);
}
@ -38,26 +39,26 @@ class DefinitionResolverTest extends TestCase
public function testGetDefinedFqnForIncompleteDefine()
{
// define('XXX') (only one argument) must not introduce a new symbol
$parser = new Parser;
$stmts = $parser->parse("<?php\ndefine('TEST_DEFINE');");
$stmts[0]->setAttribute('ownerDocument', new MockPhpDocument);
$parser = new PhpParser\Parser;
$doc = new MockPhpDocument;
$stmts = $parser->parseSourceFile("<?php\ndefine('TEST_DEFINE');", $doc->getUri());
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$fqn = $definitionResolver->getDefinedFqn($stmts[0]);
$fqn = $definitionResolver->getDefinedFqn($stmts->statementList[1]->expression);
$this->assertNull($fqn);
}
public function testGetDefinedFqnForDefine()
{
$parser = new Parser;
$stmts = $parser->parse("<?php\ndefine('TEST_DEFINE', true);");
$stmts[0]->setAttribute('ownerDocument', new MockPhpDocument);
$parser = new PhpParser\Parser;
$doc = new MockPhpDocument;
$stmts = $parser->parseSourceFile("<?php\ndefine('TEST_DEFINE', true);", $doc->getUri());
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$fqn = $definitionResolver->getDefinedFqn($stmts[0]);
$fqn = $definitionResolver->getDefinedFqn($stmts->statementList[1]->expression);
$this->assertEquals('TEST_DEFINE', $fqn);
}

View File

@ -4,39 +4,21 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use PhpParser\{NodeTraverser, Node};
use PhpParser\NodeVisitor\NameResolver;
use phpDocumentor\Reflection\DocBlockFactory;
use LanguageServer\{LanguageClient, PhpDocument, PhpDocumentLoader, Parser, DefinitionResolver};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\ClientCapabilities;
use LanguageServer\Index\{ProjectIndex, Index, DependenciesIndex};
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector};
use LanguageServer\{
DefinitionResolver, TreeAnalyzer
};
use LanguageServer\Index\{Index};
use function LanguageServer\pathToUri;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
class DefinitionCollectorTest extends TestCase
{
public function testCollectsSymbols()
{
$path = realpath(__DIR__ . '/../../fixtures/symbols.php');
$uri = pathToUri($path);
$parser = new Parser;
$docBlockFactory = DocBlockFactory::createInstance();
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$content = file_get_contents($path);
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
$stmts = $parser->parse($content);
$traverser = new NodeTraverser;
$traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ReferencesAdder($document));
$definitionCollector = new DefinitionCollector($definitionResolver);
$traverser->addVisitor($definitionCollector);
$traverser->traverse($stmts);
$defNodes = $definitionCollector->nodes;
$defNodes = $this->collectDefinitions($path);
$this->assertEquals([
'TestNamespace',
@ -55,46 +37,48 @@ class DefinitionCollectorTest extends TestCase
'TestNamespace\\Example->__construct()',
'TestNamespace\\Example->__destruct()'
], array_keys($defNodes));
$this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TEST_CONST']);
$this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\TestClass']);
$this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TestClass::TEST_CLASS_CONST']);
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::$staticTestProperty']);
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass->testProperty']);
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass::staticTestMethod()']);
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass->testMethod()']);
$this->assertInstanceOf(Node\Stmt\Trait_::class, $defNodes['TestNamespace\\TestTrait']);
$this->assertInstanceOf(Node\Stmt\Interface_::class, $defNodes['TestNamespace\\TestInterface']);
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\test_function()']);
$this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\ChildClass']);
$this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\Example']);
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\Example->__construct()']);
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\Example->__destruct()']);
$this->assertInstanceOf(Node\ConstElement::class, $defNodes['TestNamespace\\TEST_CONST']);
$this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\TestClass']);
$this->assertInstanceOf(Node\ConstElement::class, $defNodes['TestNamespace\\TestClass::TEST_CLASS_CONST']);
// TODO - should we parse properties more strictly?
$this->assertInstanceOf(Node\Expression\Variable::class, $defNodes['TestNamespace\\TestClass::$staticTestProperty']);
$this->assertInstanceOf(Node\Expression\Variable::class, $defNodes['TestNamespace\\TestClass->testProperty']);
$this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\TestClass::staticTestMethod()']);
$this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\TestClass->testMethod()']);
$this->assertInstanceOf(Node\Statement\TraitDeclaration::class, $defNodes['TestNamespace\\TestTrait']);
$this->assertInstanceOf(Node\Statement\InterfaceDeclaration::class, $defNodes['TestNamespace\\TestInterface']);
$this->assertInstanceOf(Node\Statement\FunctionDeclaration::class, $defNodes['TestNamespace\\test_function()']);
$this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\ChildClass']);
$this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\Example']);
$this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__construct()']);
$this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__destruct()']);
}
public function testDoesNotCollectReferences()
{
$path = realpath(__DIR__ . '/../../fixtures/references.php');
$defNodes = $this->collectDefinitions($path);
$this->assertEquals(['TestNamespace', 'TestNamespace\\whatever()'], array_keys($defNodes));
$this->assertInstanceOf(Node\Statement\NamespaceDefinition::class, $defNodes['TestNamespace']);
$this->assertInstanceOf(Node\Statement\FunctionDeclaration::class, $defNodes['TestNamespace\\whatever()']);
}
/**
* @param $path
*/
private function collectDefinitions(string $path): array
{
$uri = pathToUri($path);
$parser = new Parser;
$parser = new PhpParser\Parser();
$docBlockFactory = DocBlockFactory::createInstance();
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$content = file_get_contents($path);
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
$stmts = $parser->parse($content);
$traverser = new NodeTraverser;
$traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ReferencesAdder($document));
$definitionCollector = new DefinitionCollector($definitionResolver);
$traverser->addVisitor($definitionCollector);
$traverser->traverse($stmts);
$defNodes = $definitionCollector->nodes;
$this->assertEquals(['TestNamespace', 'TestNamespace\\whatever()'], array_keys($defNodes));
$this->assertInstanceOf(Node\Name::class, $defNodes['TestNamespace']);
$this->assertInstanceOf(Node\Stmt\Namespace_::class, $defNodes['TestNamespace']->getAttribute('parentNode'));
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']);
$treeAnalyzer = new TreeAnalyzer($parser, $content, $docBlockFactory, $definitionResolver, $uri);
return $treeAnalyzer->getDefinitionNodes();
}
}

View File

@ -3,20 +3,14 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{
TextDocumentItem,
TextDocumentIdentifier,
SymbolKind,
DiagnosticSeverity,
FormattingOptions,
ClientCapabilities
use LanguageServer\{
PhpDocument, PhpDocumentLoader, Project, DefinitionResolver
};
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{
DependenciesIndex, Index, ProjectIndex
};
use PHPUnit\Framework\TestCase;
use function LanguageServer\pathToUri;
class PhpDocumentLoaderTest extends TestCase

View File

@ -3,22 +3,26 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase;
use LanguageServer\{
PhpDocument, DefinitionResolver
};
use LanguageServer\Index\{
Index
};
use LanguageServer\Protocol\{
Position
};
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use phpDocumentor\Reflection\DocBlockFactory;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{LanguageClient, PhpDocument, DefinitionResolver, Parser};
use LanguageServer\NodeVisitor\NodeAtPositionFinder;
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use PhpParser\Node;
use PHPUnit\Framework\TestCase;
use function LanguageServer\isVendored;
class PhpDocumentTest extends TestCase
{
public function createDocument(string $uri, string $content)
{
$parser = new Parser;
$parser = new PhpParser\Parser();
$docBlockFactory = DocBlockFactory::createInstance();
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
@ -36,10 +40,15 @@ class PhpDocumentTest extends TestCase
{
$document = $this->createDocument('whatever', "<?php\n$\$a = new SomeClass;");
$node = $document->getNodeAtPosition(new Position(1, 13));
$this->assertInstanceOf(Node\Name\FullyQualified::class, $node);
$this->assertQualifiedName($node);
$this->assertEquals('SomeClass', (string)$node);
}
private function assertQualifiedName($node)
{
$this->assertInstanceOf(Node\QualifiedName::class, $node);
}
public function testIsVendored()
{
$document = $this->createDocument('file:///dir/vendor/x.php', "<?php\n$\$a = new SomeClass;");

View File

@ -5,12 +5,13 @@ namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\Index\{ProjectIndex, StubsIndex, GlobalIndex, DependenciesIndex, Index};
use LanguageServer\{
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities};
use LanguageServer\Protocol\{Position, Location, Range};
use function LanguageServer\pathToUri;
use Sabre\Event\Promise;
abstract class ServerTestCase extends TestCase
{
@ -87,13 +88,13 @@ abstract class ServerTestCase extends TestCase
'whatever()' => new Location($globalReferencesUri, new Range(new Position(21, 0), new Position(23, 1))),
// Namespaced
'TestNamespace' => new Location($symbolsUri, new Range(new Position( 2, 10), new Position( 2, 23))),
'SecondTestNamespace' => new Location($useUri, new Range(new Position( 2, 10), new Position( 2, 29))),
'TestNamespace\\TEST_CONST' => new Location($symbolsUri, new Range(new Position( 9, 6), new Position( 9, 22))),
'TestNamespace\\TestClass' => new Location($symbolsUri, new Range(new Position(20, 0), new Position(61, 1))),
'TestNamespace\\ChildClass' => new Location($symbolsUri, new Range(new Position(99, 0), new Position(99, 37))),
'TestNamespace\\TestTrait' => new Location($symbolsUri, new Range(new Position(63, 0), new Position(66, 1))),
'TestNamespace\\TestInterface' => new Location($symbolsUri, new Range(new Position(68, 0), new Position(71, 1))),
'TestNamespace' => new Location($symbolsUri, new Range(new Position( 2, 0), new Position( 2, 24))),
'SecondTestNamespace' => new Location($useUri, new Range(new Position( 2, 0), new Position( 2, 30))),
'TestNamespace\\TEST_CONST' => new Location($symbolsUri, new Range(new Position( 9, 6), new Position( 9, 22))),
'TestNamespace\\TestClass' => new Location($symbolsUri, new Range(new Position(20, 0), new Position(61, 1))),
'TestNamespace\\ChildClass' => new Location($symbolsUri, new Range(new Position(99, 0), new Position(99, 37))),
'TestNamespace\\TestTrait' => new Location($symbolsUri, new Range(new Position(63, 0), new Position(66, 1))),
'TestNamespace\\TestInterface' => new Location($symbolsUri, new Range(new Position(68, 0), new Position(71, 1))),
'TestNamespace\\TestClass::TEST_CLASS_CONST' => new Location($symbolsUri, new Range(new Position(27, 10), new Position(27, 32))),
'TestNamespace\\TestClass::testProperty' => new Location($symbolsUri, new Range(new Position(41, 11), new Position(41, 24))),
'TestNamespace\\TestClass::staticTestProperty' => new Location($symbolsUri, new Range(new Position(34, 18), new Position(34, 37))),
@ -112,7 +113,7 @@ abstract class ServerTestCase extends TestCase
'TestNamespace' => [
0 => new Location($referencesUri, new Range(new Position(31, 13), new Position(31, 40))), // use function TestNamespace\test_function;
1 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass;
2 => new Location($useUri, new Range(new Position( 5, 4), new Position( 5, 17))) // use TestNamespace\{TestTrait, TestInterface};
2 => new Location($useUri, new Range(new Position( 5, 4), new Position( 5, 18))) // use TestNamespace\{TestTrait, TestInterface};
],
'TestNamespace\\TEST_CONST' => [
0 => new Location($referencesUri, new Range(new Position(29, 5), new Position(29, 15)))
@ -147,16 +148,16 @@ abstract class ServerTestCase extends TestCase
3 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 49))) // TestClass::$staticTestProperty[123]->testProperty;
],
'TestNamespace\\TestClass::staticTestProperty' => [
0 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 35))), // echo TestClass::$staticTestProperty;
1 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty;
0 => new Location($referencesUri, new Range(new Position( 8, 16), new Position( 8, 35))), // echo TestClass::$staticTestProperty;
1 => new Location($referencesUri, new Range(new Position(39, 11), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty;
],
'TestNamespace\\TestClass::staticTestMethod()' => [
0 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 29)))
0 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 27)))
],
'TestNamespace\\TestClass::testMethod()' => [
0 => new Location($referencesUri, new Range(new Position( 5, 0), new Position( 5, 18))), // $obj->testMethod();
1 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 32))), // $obj->testProperty->testMethod();
2 => new Location($referencesUri, new Range(new Position(42, 5), new Position(42, 25))) // $child->testMethod();
0 => new Location($referencesUri, new Range(new Position( 5, 0), new Position( 5, 16))), // $obj->testMethod();
1 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 30))), // $obj->testProperty->testMethod();
2 => new Location($referencesUri, new Range(new Position(42, 5), new Position(42, 23))) // $child->testMethod();
],
'TestNamespace\\test_function()' => [
0 => new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))),
@ -186,7 +187,7 @@ abstract class ServerTestCase extends TestCase
],
'TestInterface' => [
0 => new Location($globalSymbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface
1 => new Location($globalSymbolsUri, new Range(new Position(57, 48), new Position(57, 61))), // public function testMethod($testParameter): TestInterface
1 => new Location($globalSymbolsUri, new Range(new Position(57, 49), new Position(57, 61))), // public function testMethod($testParameter) : TestInterface
2 => new Location($globalReferencesUri, new Range(new Position(33, 20), new Position(33, 33))) // if ($abc instanceof TestInterface)
],
'TestClass::TEST_CLASS_CONST' => [
@ -200,16 +201,16 @@ abstract class ServerTestCase extends TestCase
3 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 49))) // TestClass::$staticTestProperty[123]->testProperty;
],
'TestClass::staticTestProperty' => [
0 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 35))), // echo TestClass::$staticTestProperty;
1 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty;
0 => new Location($globalReferencesUri, new Range(new Position( 8, 16), new Position( 8, 35))), // echo TestClass::$staticTestProperty;
1 => new Location($globalReferencesUri, new Range(new Position(39, 11), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty;
],
'TestClass::staticTestMethod()' => [
0 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 29)))
0 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 27)))
],
'TestClass::testMethod()' => [
0 => new Location($globalReferencesUri, new Range(new Position( 5, 0), new Position( 5, 18))), // $obj->testMethod();
1 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 32))), // $obj->testProperty->testMethod();
2 => new Location($globalReferencesUri, new Range(new Position(42, 5), new Position(42, 25))) // $child->testMethod();
0 => new Location($globalReferencesUri, new Range(new Position( 5, 0), new Position( 5, 16))), // $obj->testMethod();
1 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 30))), // $obj->testProperty->testMethod();
2 => new Location($globalReferencesUri, new Range(new Position(42, 5), new Position(42, 23))) // $child->testMethod();
],
'test_function()' => [
0 => new Location($globalReferencesUri, new Range(new Position(10, 0), new Position(10, 13))),

View File

@ -5,15 +5,16 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, CompletionProvider, DefinitionResolver};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex, GlobalIndex, StubsIndex};
use LanguageServer\{
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{
TextDocumentIdentifier,
TextEdit,
Range,
Position,
ClientCapabilities,
CompletionList,
CompletionItem,
CompletionItemKind
@ -52,7 +53,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(3, 7)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'testProperty',
CompletionItemKind::PROPERTY,
@ -76,7 +77,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(3, 6)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'testProperty',
CompletionItemKind::PROPERTY,
@ -100,7 +101,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(8, 5)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'$var',
CompletionItemKind::VARIABLE,
@ -132,7 +133,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(8, 6)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'$param',
CompletionItemKind::VARIABLE,
@ -154,7 +155,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(6, 10)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
// Global TestClass definition (inserted as \TestClass)
new CompletionItem(
'TestClass',
@ -223,17 +224,20 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(6, 5)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'TestClass',
CompletionItemKind::CLASS_,
'TestNamespace',
'Pariatur ut laborum tempor voluptate consequat ea deserunt.' . "\n\n" .
'Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud' . "\n" .
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .
'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" .
'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" .
'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.'
'Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud' . "\n" .
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .
'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" .
'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" .
'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.',
null,
null,
'TestClass'
)
], true), $items);
}
@ -246,7 +250,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(2, 14)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'staticTestProperty',
CompletionItemKind::PROPERTY,
@ -267,7 +271,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(2, 11)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'TEST_CLASS_CONST',
CompletionItemKind::VARIABLE,
@ -300,7 +304,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(2, 13)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'TEST_CLASS_CONST',
CompletionItemKind::VARIABLE,
@ -333,7 +337,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(2, 13)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'TEST_CLASS_CONST',
CompletionItemKind::VARIABLE,
@ -366,7 +370,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(6, 6)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'TestClass',
CompletionItemKind::CLASS_,
@ -392,7 +396,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(2, 1)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem('class', CompletionItemKind::KEYWORD, null, null, null, null, 'class '),
new CompletionItem('clone', CompletionItemKind::KEYWORD, null, null, null, null, 'clone ')
], true), $items);
@ -406,7 +410,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(0, 0)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'<?php',
CompletionItemKind::KEYWORD,
@ -428,7 +432,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(0, 1)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'<?php',
CompletionItemKind::KEYWORD,
@ -450,7 +454,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(4, 6)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'SomeNamespace',
CompletionItemKind::MODULE,
@ -471,7 +475,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri),
new Position(4, 8)
)->wait();
$this->assertEquals(new CompletionList([
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'$abc2',
CompletionItemKind::VARIABLE,
@ -494,4 +498,13 @@ class CompletionTest extends TestCase
)
], true), $items);
}
private function assertCompletionsListSubset(CompletionList $subsetList, CompletionList $list)
{
foreach ($subsetList->items as $expectedItem) {
$this->assertContains($expectedItem, $list->items, null, null, false);
}
$this->assertEquals($subsetList->isIncomplete, $list->isIncomplete);
}
}

View File

@ -5,11 +5,12 @@ namespace LanguageServer\Tests\Server\TextDocument\Definition;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\Tests\Server\ServerTestCase;
use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\{
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location, ClientCapabilities};
use Sabre\Event\Promise;
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location};
class GlobalFallbackTest extends ServerTestCase
{

View File

@ -24,7 +24,7 @@ class GlobalTest extends ServerTestCase
// namespace keyword
$result = $this->textDocument->definition(
new TextDocumentIdentifier(pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'))),
new Position(2, 4)
new Position(1, 0)
)->wait();
$this->assertEquals([], $result);
}

View File

@ -5,17 +5,16 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\{
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{
TextDocumentIdentifier,
TextDocumentItem,
VersionedTextDocumentIdentifier,
TextDocumentContentChangeEvent,
Range,
Position,
ClientCapabilities
Position
};
class DidChangeTest extends TestCase

View File

@ -5,11 +5,12 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\{
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, ClientCapabilities};
use Exception;
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier};
class DidCloseTest extends TestCase
{

View File

@ -5,14 +5,15 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\{
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{
TextDocumentIdentifier,
TextDocumentItem,
FormattingOptions,
ClientCapabilities,
TextEdit,
Range,
Position

View File

@ -21,7 +21,7 @@ class HoverTest extends ServerTestCase
$reference->range->start
)->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\nclass TestClass implements \\TestInterface"),
new MarkedString('php', "<?php\nclass TestClass implements TestInterface"),
'Pariatur ut laborum tempor voluptate consequat ea deserunt.' . "\n\n" .
'Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud' . "\n" .
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .
@ -41,7 +41,7 @@ class HoverTest extends ServerTestCase
$definition->range->start
)->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\nclass TestClass implements \\TestInterface"),
new MarkedString('php', "<?php\nclass TestClass implements TestInterface"),
'Pariatur ut laborum tempor voluptate consequat ea deserunt.' . "\n\n" .
'Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud' . "\n" .
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .
@ -61,7 +61,7 @@ class HoverTest extends ServerTestCase
$reference->range->end
)->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\npublic function testMethod(\$testParameter) : \TestInterface"),
new MarkedString('php', "<?php\npublic function testMethod(\$testParameter): TestInterface"),
'Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.'
], $reference->range), $result);
}
@ -165,8 +165,9 @@ class HoverTest extends ServerTestCase
new TextDocumentIdentifier($reference->uri),
$reference->range->end
)->wait();
// TODO - should pretty print with fqns, like \define, \false. Not yet supported by tolerant-php-parser
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\n\\define('TEST_DEFINE_CONSTANT', \\false);"),
new MarkedString('php', "<?php\ndefine('TEST_DEFINE_CONSTANT', false)"),
'Lorem ipsum dolor sit amet, consectetur.'
], $reference->range), $result);
}
@ -178,7 +179,7 @@ class HoverTest extends ServerTestCase
$uri = pathToUri(realpath(__DIR__ . '/../../../fixtures/references.php'));
$result = $this->textDocument->hover(new TextDocumentIdentifier($uri), new Position(13, 7))->wait();
$this->assertEquals(new Hover(
[new MarkedString('php', "<?php\n\$var = 123;")],
[new MarkedString('php', "<?php\n\$var = 123")],
new Range(new Position(13, 5), new Position(13, 9))
), $result);
}
@ -191,7 +192,7 @@ class HoverTest extends ServerTestCase
$result = $this->textDocument->hover(new TextDocumentIdentifier($uri), new Position(22, 11))->wait();
$this->assertEquals(new Hover(
[
new MarkedString('php', "<?php\n\TestNamespace\TestClass \$param"),
new MarkedString('php', "<?php\nTestClass \$param"),
'Adipisicing non non cillum sint incididunt cillum enim mollit.'
],
new Range(new Position(22, 9), new Position(22, 15))
@ -205,7 +206,7 @@ class HoverTest extends ServerTestCase
$uri = pathToUri(realpath(__DIR__ . '/../../../fixtures/global_symbols.php'));
$result = $this->textDocument->hover(new TextDocumentIdentifier($uri), new Position(59, 11))->wait();
$this->assertEquals(new Hover([
new MarkedString('php', "<?php\nclass TestClass implements \\TestInterface"),
new MarkedString('php', "<?php\nclass TestClass implements TestInterface"),
'Pariatur ut laborum tempor voluptate consequat ea deserunt.' . "\n\n" .
'Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud' . "\n" .
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .

View File

@ -5,10 +5,12 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, ClientHandler, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\{
Server, Client, LanguageClient, ClientHandler, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem, DiagnosticSeverity, ClientCapabilities};
use LanguageServer\Protocol\{TextDocumentItem, DiagnosticSeverity};
use Sabre\Event\Promise;
use JsonMapper;
@ -62,7 +64,55 @@ class ParseErrorsTest extends TestCase
'range' => [
'start' => [
'line' => 2,
'character' => 10
'character' => 9
],
'end' => [
'line' => 2,
'character' => 9
]
],
'severity' => DiagnosticSeverity::ERROR,
'code' => null,
'source' => 'php',
'message' => "'Name' expected."
],
[
'range' => [
'start' => [
'line' => 2,
'character' => 9
],
'end' => [
'line' => 2,
'character' => 9
]
],
'severity' => DiagnosticSeverity::ERROR,
'code' => null,
'source' => 'php',
'message' => "'{' expected."
],
[
'range' => [
'start' => [
'line' => 2,
'character' => 9
],
'end' => [
'line' => 2,
'character' => 9
]
],
'severity' => DiagnosticSeverity::ERROR,
'code' => null,
'source' => 'php',
'message' => "'}' expected."
],
[
'range' => [
'start' => [
'line' => 2,
'character' => 15
],
'end' => [
'line' => 2,
@ -72,13 +122,14 @@ class ParseErrorsTest extends TestCase
'severity' => DiagnosticSeverity::ERROR,
'code' => null,
'source' => 'php',
'message' => "Syntax error, unexpected T_CLASS, expecting T_STRING"
'message' => "'Name' expected."
]]
], json_decode(json_encode($this->args), true));
}
public function testParseErrorsWithOnlyStartLine()
{
$this->markTestIncomplete('This diagnostic not yet implemented in tolerant-php-parser');
$this->openFile(__DIR__ . '/../../../fixtures/namespace_not_first.php');
$this->assertEquals([
'whatever',

View File

@ -3,12 +3,17 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Server\TextDocument\References;
use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\{
LanguageClient, PhpDocumentLoader, Server, DefinitionResolver
};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, ReferenceContext, Location, Range, ClientCapabilities};
use LanguageServer\Index\{
DependenciesIndex, Index, ProjectIndex
};
use LanguageServer\Protocol\{
Location, Position, Range, ReferenceContext, TextDocumentIdentifier
};
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\Tests\Server\ServerTestCase;
class GlobalFallbackTest extends ServerTestCase

View File

@ -29,7 +29,7 @@ class SymbolTest extends ServerTestCase
$referencesUri = pathToUri(realpath(__DIR__ . '/../../../fixtures/references.php'));
// @codingStandardsIgnoreStart
$this->assertEquals([
new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, new Location($referencesUri, new Range(new Position(2, 10), new Position(2, 23))), ''),
new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, new Location($referencesUri, new Range(new Position(2, 0), new Position(2, 24))), ''),
// Namespaced
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'),
new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'),

View File

@ -0,0 +1,155 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests;
use Exception;
use LanguageServer\Definition;
use LanguageServer\Index\Index;
use LanguageServer\PhpDocument;
use LanguageServer\DefinitionResolver;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\DocBlockFactory;
use PHPUnit\Framework\TestCase;
use LanguageServer\ClientHandler;
use LanguageServer\Protocol\Message;
use AdvancedJsonRpc;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Sabre\Event\Loop;
use Microsoft\PhpParser;
class ValidationTest extends TestCase
{
public function validationTestProvider()
{
$testProviderArray = array();
$testCasesDir = realpath(__DIR__ . '/cases');
$iterator = new RecursiveDirectoryIterator($testCasesDir);
$disabled = json_decode(file_get_contents(__DIR__ . '/disabled.json'));
foreach (new RecursiveIteratorIterator($iterator) as $file) {
if (strpos(\strrev((string)$file), \strrev(".php")) === 0 && !\in_array(basename((string)$file), $disabled)) {
if ($file->getSize() < 100000) {
$testProviderArray[] = [$file->getPathname()];
}
}
}
return $testProviderArray;
}
/**
* This test loads the test cases specified in .php files under cases/ and looks at the whole set of
* Definitions and References produced. It reads the expected results from associated .json files
* and compares to the actual result. If they don't match, the test fails and it writes the new baseline
* to the .json file.
* @group validation
* @dataProvider validationTestProvider
* @param $testCaseFile
*/
public function testDefinitionsAndReferences($testCaseFile)
{
$fileContents = file_get_contents($testCaseFile);
$actualValues = $this->getActualTestValues($testCaseFile, $fileContents);
$outputFile = getExpectedValuesFile($testCaseFile);
if (!file_exists($outputFile)) {
file_put_contents($outputFile, json_encode($actualValues, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
}
$expectedValues = (array)json_decode(file_get_contents($outputFile));
try {
$this->assertEquals($expectedValues['definitions'], $actualValues['definitions']);
try {
$this->assertArraySubset((array)$expectedValues['references'], (array)$actualValues['references'], false, 'references don\'t match.');
} catch (\Throwable $e) {
$this->assertEquals((array)$expectedValues['references'], (array)$actualValues['references'], 'references don\'t match.');
}
} catch (\Throwable $e) {
$outputFile = getExpectedValuesFile($testCaseFile);
file_put_contents($outputFile, json_encode($actualValues, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
throw $e;
}
}
private function getActualTestValues($filename, $fileContents): array
{
$index = new Index();
$parser = new PhpParser\Parser();
$docBlockFactory = DocBlockFactory::createInstance();
$definitionResolver = new DefinitionResolver($index);
$document = new PhpDocument($filename, $fileContents, $index, $parser, $docBlockFactory, $definitionResolver);
$actualRefs = $index->getReferences();
$actualDefs = $this->getTestValuesFromDefs($document->getDefinitions());
// There's probably a more PHP-typical way to do this. Need to compare the objects parsed from json files
// to the real objects.
$refsAndDefs = array(
'references' => json_decode(json_encode($actualRefs)),
'definitions' => json_decode(json_encode($actualDefs))
);
// Turn references into relative paths
$testCasesDir = realpath(__DIR__ . '/cases');
foreach ($refsAndDefs['references'] as $key => $list) {
$fixedPathRefs = array_map(function ($ref) use ($testCasesDir) {
return str_replace($testCasesDir, '.', $ref);
}, $list);
$refsAndDefs['references']->$key = $fixedPathRefs;
}
// Turn def locations into relative paths
foreach ($refsAndDefs['definitions'] as $key => $def) {
if ($def !== null && $def->symbolInformation !== null &&
$def->symbolInformation->location !== null && $def->symbolInformation->location->uri !== null) {
$def->symbolInformation->location->uri = str_replace($testCasesDir, '.', $def->symbolInformation->location->uri);
}
}
return $refsAndDefs;
}
/**
* @param $definitions Definition[]
* @return array|\array[]
*/
private function getTestValuesFromDefs($definitions): array
{
$propertyNames = get_class_vars(Definition::class);
$defsForAssert = [];
foreach ($definitions as $definition) {
$fqn = $definition->fqn;
foreach ($propertyNames as $propertyName => $value) {
if ($propertyName === 'symbolInformation') {
// Range is very often different - don't check it, for now
unset($definition->$propertyName->location->range);
} elseif ($propertyName === 'extends') {
$definition->$propertyName = $definition->$propertyName ?? [];
} elseif ($propertyName === 'type' && $definition->type !== null) {
// Class info is not captured by json_encode. It's important for 'type'.
$defsForAssert[$fqn]['type__class'] = get_class($definition->type);
}
$defsForAssert[$fqn][$propertyName] = $definition->$propertyName;
}
}
return $defsForAssert;
}
}
function getExpectedValuesFile($testCaseFile): string
{
return $testCaseFile . '.expected.json';
}

View File

@ -0,0 +1,18 @@
<?php
namespace Fixtures\Prophecy;
class WithReturnTypehints extends EmptyClass
{
public function getSelf(): self {
return $this;
}
public function getName(): string {
return __CLASS__;
}
public function getParent(): parent {
return $this;
}
}

View File

@ -0,0 +1,110 @@
{
"references": {
"Fixtures\\Prophecy\\EmptyClass": [
"./WithReturnTypehints.php"
],
"self": [
"./WithReturnTypehints.php"
],
"parent": [
"./WithReturnTypehints.php"
]
},
"definitions": {
"Fixtures\\Prophecy": {
"fqn": "Fixtures\\Prophecy",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "Fixtures\\Prophecy",
"kind": 3,
"location": {
"uri": "./WithReturnTypehints.php"
},
"containerName": "Fixtures"
},
"type": null,
"declarationLine": "namespace Fixtures\\Prophecy;",
"documentation": null
},
"Fixtures\\Prophecy\\WithReturnTypehints": {
"fqn": "Fixtures\\Prophecy\\WithReturnTypehints",
"extends": [
"Fixtures\\Prophecy\\EmptyClass"
],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "WithReturnTypehints",
"kind": 5,
"location": {
"uri": "./WithReturnTypehints.php"
},
"containerName": "Fixtures\\Prophecy"
},
"type": null,
"declarationLine": "class WithReturnTypehints extends EmptyClass",
"documentation": null
},
"Fixtures\\Prophecy\\WithReturnTypehints->getSelf()": {
"fqn": "Fixtures\\Prophecy\\WithReturnTypehints->getSelf()",
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "getSelf",
"kind": 6,
"location": {
"uri": "./WithReturnTypehints.php"
},
"containerName": "Fixtures\\Prophecy\\WithReturnTypehints"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Object_",
"type": {},
"declarationLine": "public function getSelf(): self {",
"documentation": null
},
"Fixtures\\Prophecy\\WithReturnTypehints->getName()": {
"fqn": "Fixtures\\Prophecy\\WithReturnTypehints->getName()",
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "getName",
"kind": 6,
"location": {
"uri": "./WithReturnTypehints.php"
},
"containerName": "Fixtures\\Prophecy\\WithReturnTypehints"
},
"type__class": "phpDocumentor\\Reflection\\Types\\String_",
"type": {},
"declarationLine": "public function getName(): string {",
"documentation": null
},
"Fixtures\\Prophecy\\WithReturnTypehints->getParent()": {
"fqn": "Fixtures\\Prophecy\\WithReturnTypehints->getParent()",
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "getParent",
"kind": 6,
"location": {
"uri": "./WithReturnTypehints.php"
},
"containerName": "Fixtures\\Prophecy\\WithReturnTypehints"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Object_",
"type": {},
"declarationLine": "public function getParent(): parent {",
"documentation": null
}
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace MyNamespace;
$a = new class () {
public $a;
const HI = 3;
function b () {
}
};

View File

@ -0,0 +1,23 @@
{
"references": [],
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./anonymousClassMembersShouldNotBeSymbols.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
}
}
}

View File

@ -0,0 +1,4 @@
<?php
class A {
protected $foo = ['hello' => TRUE];
}

View File

@ -0,0 +1,42 @@
{
"references": [],
"definitions": {
"A": {
"fqn": "A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "A",
"kind": 5,
"location": {
"uri": "./arrayValueShouldBeBoolean.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "class A {",
"documentation": null
},
"A->foo": {
"fqn": "A->foo",
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "foo",
"kind": 7,
"location": {
"uri": "./arrayValueShouldBeBoolean.php"
},
"containerName": "A"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Array_",
"type": {},
"declarationLine": "protected $foo;",
"documentation": null
}
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace MyNamespace;
switch ($a) {
case A:
}

View File

@ -0,0 +1,30 @@
{
"references": {
"MyNamespace\\A": [
"./caseStatement1.php"
],
"A": [
"./caseStatement1.php"
]
},
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./caseStatement1.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
}
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace TestNamespace;
$a = new A;
echo $a->a;
class A {
public $a = 3;
}

View File

@ -0,0 +1,67 @@
{
"references": {
"TestNamespace\\A": [
"./classDefinition1.php"
],
"TestNamespace\\A->a": [
"./classDefinition1.php"
]
},
"definitions": {
"TestNamespace": {
"fqn": "TestNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "TestNamespace",
"kind": 3,
"location": {
"uri": "./classDefinition1.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace TestNamespace;",
"documentation": null
},
"TestNamespace\\A": {
"fqn": "TestNamespace\\A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "A",
"kind": 5,
"location": {
"uri": "./classDefinition1.php"
},
"containerName": "TestNamespace"
},
"type": null,
"declarationLine": "class A {",
"documentation": null
},
"TestNamespace\\A->a": {
"fqn": "TestNamespace\\A->a",
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "a",
"kind": 7,
"location": {
"uri": "./classDefinition1.php"
},
"containerName": "TestNamespace\\A"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Integer",
"type": {},
"declarationLine": "public $a;",
"documentation": null
}
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace TestNamespace;
use SomeNamespace\Goo;
class TestClass
{
public $testProperty;
public function testMethod($testParameter)
{
$testVariable = 123;
if (empty($testParameter)) {
echo 'Empty';
}
}
}

View File

@ -0,0 +1,86 @@
{
"references": {
"SomeNamespace\\Goo": [
"./classProperty1.php"
],
"SomeNamespace": [
"./classProperty1.php"
]
},
"definitions": {
"TestNamespace": {
"fqn": "TestNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "TestNamespace",
"kind": 3,
"location": {
"uri": "./classProperty1.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace TestNamespace;",
"documentation": null
},
"TestNamespace\\TestClass": {
"fqn": "TestNamespace\\TestClass",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "TestClass",
"kind": 5,
"location": {
"uri": "./classProperty1.php"
},
"containerName": "TestNamespace"
},
"type": null,
"declarationLine": "class TestClass",
"documentation": null
},
"TestNamespace\\TestClass->testProperty": {
"fqn": "TestNamespace\\TestClass->testProperty",
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "testProperty",
"kind": 7,
"location": {
"uri": "./classProperty1.php"
},
"containerName": "TestNamespace\\TestClass"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"declarationLine": "public $testProperty;",
"documentation": null
},
"TestNamespace\\TestClass->testMethod()": {
"fqn": "TestNamespace\\TestClass->testMethod()",
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "testMethod",
"kind": 6,
"location": {
"uri": "./classProperty1.php"
},
"containerName": "TestNamespace\\TestClass"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"declarationLine": "public function testMethod($testParameter)",
"documentation": null
}
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace MyNamespace;
class A
{
public static function suite()
{
return [
"hi" => BYE
];
}
}

View File

@ -0,0 +1,67 @@
{
"references": {
"MyNamespace\\BYE": [
"./constants.php"
],
"BYE": [
"./constants.php"
]
},
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./constants.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
},
"MyNamespace\\A": {
"fqn": "MyNamespace\\A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "A",
"kind": 5,
"location": {
"uri": "./constants.php"
},
"containerName": "MyNamespace"
},
"type": null,
"declarationLine": "class A",
"documentation": null
},
"MyNamespace\\A::suite()": {
"fqn": "MyNamespace\\A::suite()",
"extends": [],
"isGlobal": false,
"isStatic": true,
"canBeInstantiated": false,
"symbolInformation": {
"name": "suite",
"kind": 6,
"location": {
"uri": "./constants.php"
},
"containerName": "MyNamespace\\A"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"declarationLine": "public static function suite()",
"documentation": null
}
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace MyNamespace;
class A
{
public static function suite()
{
return [
BYE => "hi"
];
}
}

View File

@ -0,0 +1,67 @@
{
"references": {
"MyNamespace\\BYE": [
"./constants2.php"
],
"BYE": [
"./constants2.php"
]
},
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./constants2.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
},
"MyNamespace\\A": {
"fqn": "MyNamespace\\A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "A",
"kind": 5,
"location": {
"uri": "./constants2.php"
},
"containerName": "MyNamespace"
},
"type": null,
"declarationLine": "class A",
"documentation": null
},
"MyNamespace\\A::suite()": {
"fqn": "MyNamespace\\A::suite()",
"extends": [],
"isGlobal": false,
"isStatic": true,
"canBeInstantiated": false,
"symbolInformation": {
"name": "suite",
"kind": 6,
"location": {
"uri": "./constants2.php"
},
"containerName": "MyNamespace\\A"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"declarationLine": "public static function suite()",
"documentation": null
}
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace MyNamespace;
class A
{
public static function suite()
{
return array(T_NEW);
}
}

View File

@ -0,0 +1,67 @@
{
"references": {
"MyNamespace\\T_NEW": [
"./constants3.php"
],
"T_NEW": [
"./constants3.php"
]
},
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./constants3.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
},
"MyNamespace\\A": {
"fqn": "MyNamespace\\A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "A",
"kind": 5,
"location": {
"uri": "./constants3.php"
},
"containerName": "MyNamespace"
},
"type": null,
"declarationLine": "class A",
"documentation": null
},
"MyNamespace\\A::suite()": {
"fqn": "MyNamespace\\A::suite()",
"extends": [],
"isGlobal": false,
"isStatic": true,
"canBeInstantiated": false,
"symbolInformation": {
"name": "suite",
"kind": 6,
"location": {
"uri": "./constants3.php"
},
"containerName": "MyNamespace\\A"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"declarationLine": "public static function suite()",
"documentation": null
}
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace MyNamespace;
class A
{
public function suite()
{
return HI;
}
}

View File

@ -0,0 +1,67 @@
{
"references": {
"MyNamespace\\HI": [
"./constants4.php"
],
"HI": [
"./constants4.php"
]
},
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./constants4.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
},
"MyNamespace\\A": {
"fqn": "MyNamespace\\A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "A",
"kind": 5,
"location": {
"uri": "./constants4.php"
},
"containerName": "MyNamespace"
},
"type": null,
"declarationLine": "class A",
"documentation": null
},
"MyNamespace\\A->suite()": {
"fqn": "MyNamespace\\A->suite()",
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "suite",
"kind": 6,
"location": {
"uri": "./constants4.php"
},
"containerName": "MyNamespace\\A"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"declarationLine": "public function suite()",
"documentation": null
}
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace MyNamespace;
class Mbstring
{
const MB_CASE_FOLD = PHP_INT_MAX;
}

View File

@ -0,0 +1,64 @@
{
"references": {
"MyNamespace\\PHP_INT_MAX": [
"./constants5.php"
]
},
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./constants5.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
},
"MyNamespace\\Mbstring": {
"fqn": "MyNamespace\\Mbstring",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "Mbstring",
"kind": 5,
"location": {
"uri": "./constants5.php"
},
"containerName": "MyNamespace"
},
"type": null,
"declarationLine": "class Mbstring",
"documentation": null
},
"MyNamespace\\Mbstring::MB_CASE_FOLD": {
"fqn": "MyNamespace\\Mbstring::MB_CASE_FOLD",
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MB_CASE_FOLD",
"kind": 14,
"location": {
"uri": "./constants5.php"
},
"containerName": "MyNamespace\\Mbstring"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Object_",
"type": {},
"declarationLine": "const MB_CASE_FOLD = PHP_INT_MAX;",
"documentation": null
}
}
}

View File

@ -0,0 +1,5 @@
<?php
interface A {
function b ($a = MY_CONSTANT);
}

View File

@ -0,0 +1,46 @@
{
"references": {
"MY_CONSTANT": [
"./constantsInFunctionParamDefault.php"
]
},
"definitions": {
"A": {
"fqn": "A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "A",
"kind": 11,
"location": {
"uri": "./constantsInFunctionParamDefault.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "interface A {",
"documentation": null
},
"A->b()": {
"fqn": "A->b()",
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "b",
"kind": 6,
"location": {
"uri": "./constantsInFunctionParamDefault.php"
},
"containerName": "A"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"declarationLine": "function b ($a = MY_CONSTANT);",
"documentation": null
}
}
}

View File

@ -0,0 +1,6 @@
<?php
/**
* This is a file comment, not NS comment
*/
namespace MyNamespace;

View File

@ -0,0 +1,23 @@
{
"references": [],
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./docBlocksOnNamespaceDefinition.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
}
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace MyNamespace;
try {
} catch (Exception $e) {
}

View File

@ -0,0 +1,23 @@
{
"references": [],
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./exceptions1.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
}
}
}

View File

@ -0,0 +1,12 @@
<?php
class ForLoopReference1 {
public function getThat() {
for ($that = $this; null !== $that; $that = $that->foo()) {
}
}
public function foo() {
return $this;
}
}

View File

@ -0,0 +1,60 @@
{
"references": {
"ForLoopReference1->foo()": [
"./_cases/forLoopReference1.php"
]
},
"definitions": {
"ForLoopReference1": {
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "ForLoopReference1",
"kind": 5,
"location": {
"uri": "./_cases/forLoopReference1.php"
},
"containerName": ""
},
"type__class": "LanguageServer\\Tests\\ValidationTest",
"type": null,
"documentation": null
},
"ForLoopReference1->getThat()": {
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "getThat",
"kind": 6,
"location": {
"uri": "./_cases/forLoopReference1.php"
},
"containerName": "ForLoopReference1"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"documentation": null
},
"ForLoopReference1->foo()": {
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "foo",
"kind": 6,
"location": {
"uri": "./_cases/forLoopReference1.php"
},
"containerName": "ForLoopReference1"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"documentation": null
}
}
}

View File

@ -0,0 +1,7 @@
<?php
$a = new A;
$b = function () use ($a) {
echo $a->b();
};

View File

@ -0,0 +1,11 @@
{
"references": {
"A": [
"./functionUse.php"
],
"A->b()": [
"./functionUse.php"
]
},
"definitions": []
}

View File

@ -0,0 +1,4 @@
<?php
use function LanguageServer\{pathToUri, timeout};

View File

@ -0,0 +1,11 @@
{
"references": {
"LanguageServer": [
"./functionUse2.php"
],
"LanguageServer\\timeout()": [
"./functionUse2.php"
]
},
"definitions": []
}

View File

@ -0,0 +1,6 @@
<?php
namespace MyNamespace;
if (A) {
}

View File

@ -0,0 +1,30 @@
{
"references": {
"MyNamespace\\A": [
"./ifStatement1.php"
],
"A": [
"./ifStatement1.php"
]
},
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./ifStatement1.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
}
}
}

View File

@ -0,0 +1,5 @@
<?php
interface A {
// props ignored in interface
var $a = 3;
}

View File

@ -0,0 +1,23 @@
{
"references": [],
"definitions": {
"A": {
"fqn": "A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "A",
"kind": 11,
"location": {
"uri": "./interfaceProperty.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "interface A {",
"documentation": null
}
}
}

View File

@ -0,0 +1,5 @@
<?php
namespace B;
echo __FILE__;

View File

@ -0,0 +1,23 @@
{
"references": [],
"definitions": {
"B": {
"fqn": "B",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "B",
"kind": 3,
"location": {
"uri": "./magicConstantsShouldBeGlobal.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace B;",
"documentation": null
}
}
}

View File

@ -0,0 +1,7 @@
<?php
class A {
private static $deprecationsTriggered = array(
__CLASS__ => true
);
}

View File

@ -0,0 +1,42 @@
{
"references": [],
"definitions": {
"A": {
"fqn": "A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "A",
"kind": 5,
"location": {
"uri": "./magicConsts.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "class A {",
"documentation": null
},
"A::$deprecationsTriggered": {
"fqn": "A::$deprecationsTriggered",
"extends": [],
"isGlobal": false,
"isStatic": true,
"canBeInstantiated": false,
"symbolInformation": {
"name": "deprecationsTriggered",
"kind": 7,
"location": {
"uri": "./magicConsts.php"
},
"containerName": "A"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Array_",
"type": {},
"declarationLine": "private static $deprecationsTriggered;",
"documentation": null
}
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace MyNamespace;
class A {
static function a() {
$b = new a;
$c = $b->a();
}
}

View File

@ -0,0 +1,67 @@
{
"references": {
"MyNamespace\\a": [
"./memberAccess1.php"
],
"MyNamespace\\a->a()": [
"./memberAccess1.php"
]
},
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./memberAccess1.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
},
"MyNamespace\\A": {
"fqn": "MyNamespace\\A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "A",
"kind": 5,
"location": {
"uri": "./memberAccess1.php"
},
"containerName": "MyNamespace"
},
"type": null,
"declarationLine": "class A {",
"documentation": null
},
"MyNamespace\\A::a()": {
"fqn": "MyNamespace\\A::a()",
"extends": [],
"isGlobal": false,
"isStatic": true,
"canBeInstantiated": false,
"symbolInformation": {
"name": "a",
"kind": 6,
"location": {
"uri": "./memberAccess1.php"
},
"containerName": "MyNamespace\\A"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"declarationLine": "static function a() {",
"documentation": null
}
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace MyNamespace;
class A {
static function a() {
$b = new a;
$c = $b->a();
}
}

View File

@ -0,0 +1,67 @@
{
"references": {
"MyNamespace\\a": [
"./memberAccess2.php"
],
"MyNamespace\\a->a()": [
"./memberAccess2.php"
]
},
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./memberAccess2.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
},
"MyNamespace\\A": {
"fqn": "MyNamespace\\A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "A",
"kind": 5,
"location": {
"uri": "./memberAccess2.php"
},
"containerName": "MyNamespace"
},
"type": null,
"declarationLine": "class A {",
"documentation": null
},
"MyNamespace\\A::a()": {
"fqn": "MyNamespace\\A::a()",
"extends": [],
"isGlobal": false,
"isStatic": true,
"canBeInstantiated": false,
"symbolInformation": {
"name": "a",
"kind": 6,
"location": {
"uri": "./memberAccess2.php"
},
"containerName": "MyNamespace\\A"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"declarationLine": "static function a() {",
"documentation": null
}
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace MyNamespace;
class A {
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixesPsr0 = ComposerStaticInitIncludePath::$prefixesPsr0;
}, null, ClassLoader::class);
}
}

View File

@ -0,0 +1,82 @@
{
"references": {
"MyNamespace\\ClassLoader": [
"./memberAccess3.php"
],
"Closure::bind()": [
"./memberAccess3.php"
],
"Closure": [
"./memberAccess3.php"
],
"MyNamespace\\ClassLoader->prefixesPsr0": [
"./memberAccess3.php"
],
"MyNamespace\\ComposerStaticInitIncludePath": [
"./memberAccess3.php"
],
"MyNamespace\\ComposerStaticInitIncludePath::$prefixesPsr0": [
"./memberAccess3.php"
],
"MyNamespace\\ClassLoader::class": [
"./memberAccess3.php"
]
},
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./memberAccess3.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
},
"MyNamespace\\A": {
"fqn": "MyNamespace\\A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "A",
"kind": 5,
"location": {
"uri": "./memberAccess3.php"
},
"containerName": "MyNamespace"
},
"type": null,
"declarationLine": "class A {",
"documentation": null
},
"MyNamespace\\A::getInitializer()": {
"fqn": "MyNamespace\\A::getInitializer()",
"extends": [],
"isGlobal": false,
"isStatic": true,
"canBeInstantiated": false,
"symbolInformation": {
"name": "getInitializer",
"kind": 6,
"location": {
"uri": "./memberAccess3.php"
},
"containerName": "MyNamespace\\A"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"declarationLine": "public static function getInitializer(ClassLoader $loader)",
"documentation": null
}
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace MyNamespace;
class A {
public function testRequest()
{
$request = Request::create((new Url('httpkernel_test.empty'))->toString());
}
}

View File

@ -0,0 +1,73 @@
{
"references": {
"MyNamespace\\Request::create()": [
"./memberAccess4.php"
],
"MyNamespace\\Request": [
"./memberAccess4.php"
],
"MyNamespace\\Url->toString()": [
"./memberAccess4.php"
],
"MyNamespace\\Url": [
"./memberAccess4.php"
]
},
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./memberAccess4.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
},
"MyNamespace\\A": {
"fqn": "MyNamespace\\A",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "A",
"kind": 5,
"location": {
"uri": "./memberAccess4.php"
},
"containerName": "MyNamespace"
},
"type": null,
"declarationLine": "class A {",
"documentation": null
},
"MyNamespace\\A->testRequest()": {
"fqn": "MyNamespace\\A->testRequest()",
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "testRequest",
"kind": 6,
"location": {
"uri": "./memberAccess4.php"
},
"containerName": "MyNamespace\\A"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"declarationLine": "public function testRequest()",
"documentation": null
}
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace MyNamespace;
class ParseErrorsTest {
public function setUp()
{
$a = new class($this->args) { };
}
}

View File

@ -0,0 +1,60 @@
{
"references": [],
"definitions": {
"MyNamespace": {
"fqn": "MyNamespace",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "MyNamespace",
"kind": 3,
"location": {
"uri": "./memberAccess5.php"
},
"containerName": ""
},
"type": null,
"declarationLine": "namespace MyNamespace;",
"documentation": null
},
"MyNamespace\\ParseErrorsTest": {
"fqn": "MyNamespace\\ParseErrorsTest",
"extends": [],
"isGlobal": true,
"isStatic": false,
"canBeInstantiated": true,
"symbolInformation": {
"name": "ParseErrorsTest",
"kind": 5,
"location": {
"uri": "./memberAccess5.php"
},
"containerName": "MyNamespace"
},
"type": null,
"declarationLine": "class ParseErrorsTest {",
"documentation": null
},
"MyNamespace\\ParseErrorsTest->setUp()": {
"fqn": "MyNamespace\\ParseErrorsTest->setUp()",
"extends": [],
"isGlobal": false,
"isStatic": false,
"canBeInstantiated": false,
"symbolInformation": {
"name": "setUp",
"kind": 6,
"location": {
"uri": "./memberAccess5.php"
},
"containerName": "MyNamespace\\ParseErrorsTest"
},
"type__class": "phpDocumentor\\Reflection\\Types\\Mixed",
"type": {},
"declarationLine": "public function setUp()",
"documentation": null
}
}
}

Some files were not shown because too many files have changed in this diff Show More