diff --git a/src/ComposerScripts.php b/src/ComposerScripts.php index 487c39d..6cae6d7 100644 --- a/src/ComposerScripts.php +++ b/src/ComposerScripts.php @@ -45,7 +45,7 @@ class ComposerScripts } $uris = yield $finder->find("$stubsLocation/**/*.php"); - + $uris = []; foreach ($uris as $uri) { echo "Parsing $uri\n"; $content = yield $contentRetriever->retrieve($uri); diff --git a/src/DefinitionResolver.php b/src/DefinitionResolver.php index 51c5955..50aac33 100644 --- a/src/DefinitionResolver.php +++ b/src/DefinitionResolver.php @@ -34,6 +34,16 @@ class DefinitionResolver */ private $docBlockFactory; + + public $autoloadInfo; + + // The followings are autoloading properties. + public $autoloadLibraries; + public $autoloadClasses; + public $autoloadModels; + public $autoloadLanguages; + + /** * @param ReadableIndex $index */ @@ -679,6 +689,29 @@ class DefinitionResolver return new Types\Object_(new Fqsen('\\' . $classFqn)); } 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; } } } diff --git a/src/Indexer.php b/src/Indexer.php index 9f8749d..0a55b86 100644 --- a/src/Indexer.php +++ b/src/Indexer.php @@ -118,6 +118,18 @@ class Indexer /** @var string[][] */ $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) { $packageName = getPackageName($uri, $this->composerJson); if ($this->composerLock !== null && $packageName) { diff --git a/src/LanguageServer.php b/src/LanguageServer.php index 118dd93..a0a8168 100644 --- a/src/LanguageServer.php +++ b/src/LanguageServer.php @@ -232,7 +232,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher $this->composerLock, $this->composerJson ); - $indexer->index()->otherwise('\\LanguageServer\\crash'); + yield $indexer->index()->otherwise('\\LanguageServer\\crash'); } diff --git a/src/NodeVisitor/DynamicLoader.php b/src/NodeVisitor/DynamicLoader.php new file mode 100644 index 0000000..7f83500 --- /dev/null +++ b/src/NodeVisitor/DynamicLoader.php @@ -0,0 +1,301 @@ +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; + } +} diff --git a/src/PhpDocument.php b/src/PhpDocument.php index ff3cf8b..08c6865 100644 --- a/src/PhpDocument.php +++ b/src/PhpDocument.php @@ -3,6 +3,16 @@ declare(strict_types = 1); 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\Protocol\{ Diagnostic, Position, Range @@ -105,6 +115,10 @@ class PhpDocument $this->updateContent($content); } + private function isVisitingAutoload() { + return false; + } + /** * 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); - $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->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(); foreach ($this->definitions as $fqn => $definition) { diff --git a/src/PhpDocumentLoader.php b/src/PhpDocumentLoader.php index 57a7e9c..9d7b642 100644 --- a/src/PhpDocumentLoader.php +++ b/src/PhpDocumentLoader.php @@ -20,7 +20,7 @@ class PhpDocumentLoader * * @var PhpDocument */ - private $documents = []; + public $documents = []; /** * @var ContentRetriever @@ -110,7 +110,7 @@ class PhpDocumentLoader $content = yield $this->contentRetriever->retrieve($uri); $size = strlen($content); if ($size > $limit) { - throw new ContentTooLargeException($uri, $size, $limit); + return $this->create($uri, ""); } if (isset($this->documents[$uri])) { diff --git a/src/Server/TextDocument.php b/src/Server/TextDocument.php index 5a2819e..ff3646f 100644 --- a/src/Server/TextDocument.php +++ b/src/Server/TextDocument.php @@ -261,7 +261,15 @@ class TextDocument public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise { 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); if ($node === null) { return [];