Skip to content

Instantly share code, notes, and snippets.

@mniewrzal
Last active March 2, 2017 12:44
Show Gist options
  • Save mniewrzal/e741e88b487fe0034956b5901642cba7 to your computer and use it in GitHub Desktop.
Save mniewrzal/e741e88b487fe0034956b5901642cba7 to your computer and use it in GitHub Desktop.
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Cache\Cache;
use LanguageServer\FilesFinder\FilesFinder;
use LanguageServer\Index\{DependenciesIndex, Index};
use LanguageServer\Protocol\MessageType;
use Webmozart\PathUtil\Path;
use Composer\Semver\VersionParser;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
use LanguageServer\Protocol\Diagnostic;
use LanguageServer\Protocol\Range;
use LanguageServer\Protocol\Position;
class Indexer
{
/**
* @var The prefix for every cache item
*/
const CACHE_VERSION = 1;
/**
* @var FilesFinder
*/
private $filesFinder;
/**
* @var string
*/
private $rootPath;
/**
* @var LanguageClient
*/
private $client;
/**
* @var Cache
*/
private $cache;
/**
* @var DependenciesIndex
*/
private $dependenciesIndex;
/**
* @var Index
*/
private $sourceIndex;
/**
* @var PhpDocumentLoader
*/
private $documentLoader;
/**
* @var \stdClasss
*/
private $composerLock;
/**
* @var \stdClasss
*/
private $composerJson;
/**
* @param FilesFinder $filesFinder
* @param string $rootPath
* @param LanguageClient $client
* @param Cache $cache
* @param DependenciesIndex $dependenciesIndex
* @param Index $sourceIndex
* @param PhpDocumentLoader $documentLoader
* @param \stdClass|null $composerLock
*/
public function __construct(
FilesFinder $filesFinder,
string $rootPath,
LanguageClient $client,
Cache $cache,
DependenciesIndex $dependenciesIndex,
Index $sourceIndex,
PhpDocumentLoader $documentLoader,
\stdClass $composerLock = null,
\stdClass $composerJson = null
) {
$this->filesFinder = $filesFinder;
$this->rootPath = $rootPath;
$this->client = $client;
$this->cache = $cache;
$this->dependenciesIndex = $dependenciesIndex;
$this->sourceIndex = $sourceIndex;
$this->documentLoader = $documentLoader;
$this->composerLock = $composerLock;
$this->composerJson = $composerJson;
}
/**
* Will read and parse the passed source files in the project and add them to the appropiate indexes
*
* @return Promise <void>
*/
public function index(): Promise
{
return coroutine(function () {
$pattern = Path::makeAbsolute('**/*.php', $this->rootPath);
$uris = yield $this->filesFinder->find($pattern);
$count = count($uris);
$startTime = microtime(true);
$this->client->window->logMessage(MessageType::INFO, "$count files total");
/** @var string[] */
$source = [];
/** @var string[][] */
$deps = [];
foreach ($uris as $uri) {
$packageName = getPackageName($uri, $this->composerJson);
if ($this->composerLock !== null && $packageName) {
// Dependency file
if (!isset($deps[$packageName])) {
$deps[$packageName] = [];
}
$deps[$packageName][] = $uri;
} else {
// Source file
$source[] = $uri;
}
}
// Index source
// Definitions and static references
$this->client->window->logMessage(MessageType::INFO, 'Indexing project for definitions and static references');
yield $this->indexFiles($source);
$this->sourceIndex->setStaticComplete();
// Dynamic references
$this->client->window->logMessage(MessageType::INFO, 'Indexing project for dynamic references');
yield $this->indexFiles($source);
$this->sourceIndex->setComplete();
// Index dependencies
$this->client->window->logMessage(MessageType::INFO, count($deps) . ' Packages');
foreach ($deps as $packageName => $files) {
// Find version of package and check cache
$packageKey = null;
$cacheKey = null;
$index = null;
foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) {
// Check if package name matches and version is absolute
// Dynamic constraints are not cached, because they can change every time
$packageVersion = ltrim($package->version, 'v');
if ($package->name === $packageName && strpos($packageVersion, 'dev') === false) {
$packageKey = $packageName . ':' . $packageVersion;
$cacheKey = self::CACHE_VERSION . ':' . $packageKey;
// Check cache
$index = yield $this->cache->get($cacheKey);
break;
}
}
if ($index !== null) {
// Cache hit
$this->dependenciesIndex->setDependencyIndex($packageName, $index);
$this->client->window->logMessage(MessageType::INFO, "Restored $packageKey from cache");
} else {
// Cache miss
$index = $this->dependenciesIndex->getDependencyIndex($packageName);
// Index definitions and static references
$this->client->window->logMessage(MessageType::INFO, 'Indexing ' . ($packageKey ?? $packageName) . ' for definitions and static references');
yield $this->indexFiles($files);
$index->setStaticComplete();
// Index dynamic references
$this->client->window->logMessage(MessageType::INFO, 'Indexing ' . ($packageKey ?? $packageName) . ' for dynamic references');
yield $this->indexFiles($files);
$index->setComplete();
// If we know the version (cache key), save index for the dependency in the cache
if ($cacheKey !== null) {
$this->client->window->logMessage(MessageType::INFO, "Storing $packageKey in cache");
$this->cache->set($cacheKey, $index);
} else {
$this->client->window->logMessage(MessageType::WARNING, "Could not compute cache key for $packageName");
}
}
}
$duration = (int)(microtime(true) - $startTime);
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
$this->client->window->logMessage(
MessageType::INFO,
"All $count PHP files parsed in $duration seconds. $mem MiB allocated."
);
});
}
/**
* @param array $files
* @return Promise
*/
private function indexFiles(array $files): Promise
{
return coroutine(function () use ($files) {
foreach ($files as $i => $uri) {
// Skip open documents
if ($this->documentLoader->isOpen($uri)) {
continue;
}
// Give LS to the chance to handle requests while indexing
yield timeout();
$this->client->window->logMessage(MessageType::LOG, "Parsing $uri");
try {
$document = yield $this->documentLoader->load($uri);
if (!isVendored($document, $this->composerJson)) {
$this->client->textDocument->publishDiagnostics($uri, $document->getDiagnostics());
}
} catch (ContentTooLargeException $e) {
$this->client->window->logMessage(
MessageType::INFO,
"Ignoring file {$uri} because it exceeds size limit of {$e->limit} bytes ({$e->size})"
);
} catch (\Exception $e) {
$this->client->window->logMessage(MessageType::ERROR, "Error parsing $uri: " . (string)$e);
}
}
foreach ($files as $i => $uri) {
$phpDocument = $this->documentLoader->getOrLoad($uri)->wait();
$traverser = new NodeTraverser();
$t = new class() extends NodeVisitorAbstract{
private $functions;
public function __construct(){
$this->functions = [];
}
public function enterNode(Node $node) {
// collect functions/methods calls
if ($node instanceof Node\Expr\FuncCall && $node instanceof Node\Expr\MethodCall) {
$this->functions [] = $node;
}
}
public function getFunctions(){
return $this->functions;
}
};
$traverser->addVisitor($t);
$traverser->traverse($phpDocument->getStmts());
foreach ($t->getFunctions() as $node){
$fqn = $phpDocument->getDefinitionResolver()->resolveReferenceNodeToFqn($node);
$this->client->window->logMessage(MessageType::INFO, "static analysis: " . $node->name);
//if (some_checks){ //check list of params
$d = new Diagnostic("Method x has a problem", new Range(new Position(0,0), new Position(0,1)));
$this->client->textDocument->publishDiagnostics($uri, [$d]);
//}
}
}
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment