1
0
Fork 0
pull/483/merge
Alan.Li 2017-09-25 23:35:28 +00:00 committed by GitHub
commit 0a2e3c908f
8 changed files with 407 additions and 6 deletions

View File

@ -45,7 +45,7 @@ class ComposerScripts
} }
$uris = yield $finder->find("$stubsLocation/**/*.php"); $uris = yield $finder->find("$stubsLocation/**/*.php");
$uris = [];
foreach ($uris as $uri) { foreach ($uris as $uri) {
echo "Parsing $uri\n"; echo "Parsing $uri\n";
$content = yield $contentRetriever->retrieve($uri); $content = yield $contentRetriever->retrieve($uri);

View File

@ -34,6 +34,16 @@ class DefinitionResolver
*/ */
private $docBlockFactory; private $docBlockFactory;
public $autoloadInfo;
// The followings are autoloading properties.
public $autoloadLibraries;
public $autoloadClasses;
public $autoloadModels;
public $autoloadLanguages;
/** /**
* @param ReadableIndex $index * @param ReadableIndex $index
*/ */
@ -679,6 +689,29 @@ class DefinitionResolver
return new Types\Object_(new Fqsen('\\' . $classFqn)); return new Types\Object_(new Fqsen('\\' . $classFqn));
} }
return $def->type; return $def->type;
$memberSuffix = '->' . $expr->name;
if ($expr instanceof Node\Expr\MethodCall) {
$memberSuffix .= '()';
}
// Find the right class that implements the member
$implementorFqns = [$classFqn];
while ($implementorFqn = array_shift($implementorFqns)) {
// If the member FQN exists, return it
$def = $this->index->getDefinition($implementorFqn . $memberSuffix);
if ($def) {
return $def->type;
}
// Get Definition of implementor class
$implementorDef = $this->index->getDefinition($implementorFqn);
// If it doesn't exist, return the initial guess
if ($implementorDef === null) {
break;
}
// Repeat for parent class
if ($implementorDef->extends) {
foreach ($implementorDef->extends as $extends) {
$implementorFqns[] = $extends;
} }
} }
} }

View File

@ -118,6 +118,18 @@ class Indexer
/** @var string[][] */ /** @var string[][] */
$deps = []; $deps = [];
$ending = "application/config/autoload.php";
$endingLength = strlen($ending);
foreach ($uris as $uri) {
$found = (substr($uri, -$endingLength) === $ending);
// if found, put it to the beginning of the list so that it gets analyzed first.
if ($found) {
array_unshift($uris, $uri);
break;
}
}
foreach ($uris as $uri) { foreach ($uris as $uri) {
$packageName = getPackageName($uri, $this->composerJson); $packageName = getPackageName($uri, $this->composerJson);
if ($this->composerLock !== null && $packageName) { if ($this->composerLock !== null && $packageName) {

View File

@ -232,7 +232,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$this->composerLock, $this->composerLock,
$this->composerJson $this->composerJson
); );
$indexer->index()->otherwise('\\LanguageServer\\crash'); yield $indexer->index()->otherwise('\\LanguageServer\\crash');
} }

View File

@ -0,0 +1,301 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
use PhpParser\{NodeVisitorAbstract, Node};
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
use LanguageServer\{Definition, DefinitionResolver};
use LanguageServer\Protocol\{SymbolInformation, SymbolKind, Location};
/**
* Collects definitions created dynamically by framework such as CodeIgniter
*/
class DynamicLoader extends NodeVisitorAbstract
{
public $definitionCollector;
public $prettyPrinter;
public $definitionResolver;
private $collectAutoload;
public function __construct(DefinitionCollector $definitionCollector, DefinitionResolver $definitionResolver, bool $collectAutoload)
{
$this->definitionCollector = $definitionCollector;
$this->definitionResolver = $definitionResolver;
$this->collectAutoload = $collectAutoload;
$this->prettyPrinter = new PrettyPrinter;
}
public function visitAutoloadClassDeclaration(Node $node) {
if (!($node instanceof Node\Stmt\Class_)) {
return;
}
$extends = $node->extends;
if (!isset($extends->parts)) {
return;
}
$shouldAutoload = false;
foreach ($extends->parts as $part) {
// TODO: add more criteria here?
if ($part === "CI_Controller" || $part === "ST_Controller" ||
$part === "ST_Auth_Controller") {
$shouldAutoload = true;
break;
}
}
if (!$shouldAutoload) {
return;
}
if (isset($this->definitionResolver->autoloadLibraries)) {
foreach ($this->definitionResolver->autoloadLibraries as $key => $value) {
$this->createAutoloadDefinition($node, $value);
}
}
if (isset($this->definitionResolver->autoloadModels)) {
foreach ($this->definitionResolver->autoloadModels as $key => $value) {
$this->createAutoloadDefinition($node, $value);
}
}
if (isset($this->definitionResolver->autoloadHelpers)) {
foreach ($this->definitionResolver->autoloadHelpers as $key => $value) {
$this->createAutoloadDefinition($node, $value);
}
}
if (isset($this->definitionResolver->autoloadConfig)) {
foreach ($this->definitionResolver->autoloadConfig as $key => $value) {
$this->createAutoloadDefinition($node, $value);
}
}
if (isset($this->definitionResolver->autoloadLanguage)) {
foreach ($this->definitionResolver->autoloadLanguage as $key => $value) {
$this->createAutoloadDefinition($node, $value);
}
}
}
public function visitAutoloadNode(Node $node) {
// looking at array assignments.
if (!($node instanceof Node\Expr\Assign)) {
return;
}
// check left hand side.
$lhs = $node->var;
if (!($lhs instanceof Node\Expr\ArrayDimFetch)) {
return;
}
$dimFetchVar = $lhs->var;
if (!($dimFetchVar instanceof Node\Expr\Variable)) {
return;
}
if ($dimFetchVar->name !== "autoload") {
return;
}
// end of checking left hand side.
$dim = $lhs->dim;
if (!($dim instanceof Node\Scalar\String_)) {
return;
}
// TODO: support more than libraries
$target = $dim->value;
// extract right hand side.
$rhs = $node->expr;
if (!($rhs instanceof Node\Expr\Array_)) {
return;
}
// $target -> $node reference
$arrayOfLibs = $rhs->items;
foreach ($arrayOfLibs as $lib) {
$libName = $lib->value->value;
switch ($target) {
case "libraries":
$this->definitionResolver->autoloadLibraries[$libName] = $lib;
break;
case "helper":
$this->definitionResolver->autoloadHelpers[$libName] = $lib;
break;
case "config":
$this->definitionResolver->autoloadConfig[$libName] = $lib;
break;
case "model":
$this->definitionResolver->autoloadModels[$libName] = $lib;
break;
case "language":
$this->definitionResolver->autoloadLanguage[$libName] = $lib;
break;
}
}
}
public function enterNode(Node $node)
{
// handling autoloading.
if ($this->collectAutoload) {
// records autoloading fields into definition resolver.
$this->visitAutoloadNode($node);
}
// spits autoloading fields to a class that is derived from controller classes.
$this->visitAutoloadClassDeclaration($node);
// The follwoing is for handling dynamic loading. (Finished)
// check its name is 'model', 'library' or 'helper'.
if (!($node instanceof Node\Expr\MethodCall)) {
return;
}
if ($node->name !== 'model' && $node->name !== 'library' && $node->name !== 'helper') {
return;
}
// check its caller is a 'load'
if (!isset($node->var) || !isset($node->var->name) || $node->var->name !== 'load') {
return;
}
$argSize = count($node->args);
if ($argSize == 0 || $argSize == 3) { // when argSize = 3 it's loading from db
return;
}
$nameNode = NULL;
if ($node->args[0]->value instanceof Node\Scalar\String_) {
// make sure the first argument is a string.
if ($argSize == 2) {
$nameNode = $node->args[1]->value;
}
$this->createDefinition($node, $node->args[0]->value, $nameNode);
} else if ($node->args[0]->value instanceof Node\Expr\Array_) {
$elems = $node->args[0]->value->items;
foreach ($elems as $item) {
if ($item->value instanceof Node\Scalar\String_) {
$this->createDefinition($node, $item->value, $nameNode);
}
}
}
}
// copied from createDefinition and tailored.
public function createAutoloadDefinition(Node $classNode, Node $entityNode)
{
$fieldName = $entityNode->value->value;
$enclosedClass = $classNode;
$classFqn = $enclosedClass->namespacedName->toString();
$fqn = $classFqn . "->" . $fieldName;
// if we cannot find definition, just return.
if ($fqn === NULL) {
return;
}
// add fqn to nodes and definitions.
$this->definitionCollector->nodes[$fqn] = $entityNode;
// Create symbol
// $classFqnParts = preg_split('/(::|->|\\\\)/', $fqn);
// array_pop($classFqnParts);
// $classFqn = implode('\\', $classFqnParts);
$sym = new SymbolInformation($fieldName, SymbolKind::PROPERTY, Location::fromNode($entityNode), $classFqn);
// Create type
// array_push($entityParts, ucwords($enityName));
// $typeName = implode('\\', $entityParts);
$typeName = ucwords($fieldName);
$type = new Types\Object_(new Fqsen('\\' . $typeName));
// Create defintion from symbol, type and all others
$def = new Definition;
$def->canBeInstantiated = false;
$def->isGlobal = false; // TODO check the meaning of this, why public field has this set to false?
$def->isStatic = false; // it should not be a static field
$def->fqn = $fqn;
$def->symbolInformation = $sym;
$def->type = $type;
// Maybe this is not the best
$def->declarationLine = $fieldName; // $this->prettyPrinter->prettyPrint([$argNode]);
$def->documentation = "Dynamically Generated Field: " . $fieldName;
$this->definitionCollector->definitions[$fqn] = $def;
}
public function createDefinition($callNode, $entityNode, $nameNode)
{
$entityString = $entityNode->value;
$entityParts = explode('/', $entityString);
$enityName = array_pop($entityParts);
$fieldName = $enityName;
// deal with case like: $this->_CI->load->model('users_mdl', 'hahaha');
if ($callNode->name = "model" && $nameNode !== NULL) {
if (!($nameNode instanceof Node\Scalar\String_)) {
return;
}
$fieldName = $nameNode->value;
}
$enclosedClass = $callNode;
$fqn = NULL;
$classFqn = NULL;
while ($enclosedClass !== NULL) {
$enclosedClass = $enclosedClass->getAttribute('parentNode');
if ($enclosedClass instanceof Node\Stmt\ClassLike && isset($enclosedClass->name)) {
$classFqn = $enclosedClass->namespacedName->toString();
$fqn = $classFqn . '->' . $fieldName;
break;
}
}
// if we cannot find definition, just return.
if ($fqn === NULL) {
return;
}
// add fqn to nodes and definitions.
$this->definitionCollector->nodes[$fqn] = $entityNode;
// Create symbol
// $classFqnParts = preg_split('/(::|->|\\\\)/', $fqn);
// array_pop($classFqnParts);
// $classFqn = implode('\\', $classFqnParts);
$sym = new SymbolInformation($fieldName, SymbolKind::PROPERTY, Location::fromNode($entityNode), $classFqn);
// Create type
// array_push($entityParts, ucwords($enityName));
// $typeName = implode('\\', $entityParts);
$typeName = ucwords($enityName);
$type = new Types\Object_(new Fqsen('\\' . $typeName));
// Create defintion from symbol, type and all others
$def = new Definition;
$def->canBeInstantiated = false;
$def->isGlobal = false; // TODO check the meaning of this, why public field has this set to false?
$def->isStatic = false; // it should not be a static field
$def->fqn = $fqn;
$def->symbolInformation = $sym;
$def->type = $type;
// Maybe this is not the best
$def->declarationLine = $fieldName; // $this->prettyPrinter->prettyPrint([$argNode]);
$def->documentation = "Dynamically Generated Field: " . $fieldName;
$this->definitionCollector->definitions[$fqn] = $def;
}
}

View File

@ -3,6 +3,16 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit};
use LanguageServer\NodeVisitor\{
NodeAtPositionFinder,
ReferencesAdder,
DocBlockParser,
DefinitionCollector,
ColumnCalculator,
ReferencesCollector,
DynamicLoader
};
use LanguageServer\Index\Index; use LanguageServer\Index\Index;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
Diagnostic, Position, Range Diagnostic, Position, Range
@ -105,6 +115,10 @@ class PhpDocument
$this->updateContent($content); $this->updateContent($content);
} }
private function isVisitingAutoload() {
return false;
}
/** /**
* Get all references of a fully qualified name * Get all references of a fully qualified name
* *
@ -146,12 +160,45 @@ class PhpDocument
$treeAnalyzer = new TreeAnalyzer($this->parser, $content, $this->docBlockFactory, $this->definitionResolver, $this->uri); $treeAnalyzer = new TreeAnalyzer($this->parser, $content, $this->docBlockFactory, $this->definitionResolver, $this->uri);
$this->diagnostics = $treeAnalyzer->getDiagnostics(); $this->diagnostics = [];
foreach ($errorHandler->getErrors() as $error) {
$this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php');
}
// figure out if it is analyzing an autoload file.
$isAutoload = false;
$ending = "application/config/autoload.php";
$endingLength = strlen($ending);
$isAutoload = (substr($this->uri, -$endingLength) === $ending);
// $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));
$this->definitions = $treeAnalyzer->getDefinitions(); $this->definitions = $treeAnalyzer->getDefinitions();
$this->definitionNodes = $treeAnalyzer->getDefinitionNodes(); $this->definitionNodes = $treeAnalyzer->getDefinitionNodes();
// 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);
$traverser->addVisitor(new DynamicLoader($definitionCollector, $this->definitionResolver, $isAutoload));
// Collect all references
$referencesCollector = new ReferencesCollector($this->definitionResolver);
//$traverser->addVisitor($referencesCollector);
$this->referenceNodes = $treeAnalyzer->getReferenceNodes(); $this->referenceNodes = $treeAnalyzer->getReferenceNodes();
foreach ($this->definitions as $fqn => $definition) { foreach ($this->definitions as $fqn => $definition) {

View File

@ -20,7 +20,7 @@ class PhpDocumentLoader
* *
* @var PhpDocument * @var PhpDocument
*/ */
private $documents = []; public $documents = [];
/** /**
* @var ContentRetriever * @var ContentRetriever
@ -110,7 +110,7 @@ class PhpDocumentLoader
$content = yield $this->contentRetriever->retrieve($uri); $content = yield $this->contentRetriever->retrieve($uri);
$size = strlen($content); $size = strlen($content);
if ($size > $limit) { if ($size > $limit) {
throw new ContentTooLargeException($uri, $size, $limit); return $this->create($uri, "");
} }
if (isset($this->documents[$uri])) { if (isset($this->documents[$uri])) {

View File

@ -261,7 +261,15 @@ class TextDocument
public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise
{ {
return coroutine(function () use ($textDocument, $position) { return coroutine(function () use ($textDocument, $position) {
$document = yield $this->documentLoader->getOrLoad($textDocument->uri); $documentLoader = $this->documentLoader;//->getOrLoad($textDocument->uri);
$document = null;
if (isset($documentLoader->documents[$textDocument->uri])) {
$document = $documentLoader->documents[$textDocument->uri];
} else {
$document = yield $documentLoader->load($textDocument->uri);
$documentLoader->documents[$textDocument->uri] = $document;
}
$node = $document->getNodeAtPosition($position); $node = $document->getNodeAtPosition($position);
if ($node === null) { if ($node === null) {
return []; return [];