Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-2412: Support for indexing and analyzing phars #2602

Merged
merged 10 commits into from Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Expand Up @@ -126,7 +126,7 @@ jobs:
composer-options: "--no-scripts"
-
name: "Run PHPUnit"
run: "php -dzend.assertions=1 vendor/bin/phpunit"
run: "php -dphar.readonly=0 -dzend.assertions=1 vendor/bin/phpunit"
phpstan:
name: "PHPStan (${{ matrix.php-version }})"

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,10 @@ Changelog

## master

Features:

- PHAR Indexing #2412 @dantleech

Improvements:

- Basic support for `array_reduce` stub #2576
Expand Down
4 changes: 2 additions & 2 deletions doc/reference/configuration.rst
Expand Up @@ -1614,7 +1614,7 @@ Type: array
Glob patterns to include while indexing


**Default**: ``["\/**\/*.php"]``
**Default**: ``["\/**\/*.php","\/**\/*.phar"]``


.. _param_indexer.exclude_patterns:
Expand Down Expand Up @@ -1758,7 +1758,7 @@ Type: array
File extensions (e.g. `php`) for files that should be indexed


**Default**: ``["php"]``
**Default**: ``["php","phar"]``


.. _ObjectRendererExtension:
Expand Down
Expand Up @@ -20,7 +20,7 @@ public function __construct(
private Filesystem $filesystem,
private array $includePatterns = [],
private array $excludePatterns = [],
private array $supportedExtensions = ['php'],
private array $supportedExtensions = ['php', 'phar'],
) {
}

Expand Down
2 changes: 1 addition & 1 deletion lib/Indexer/Adapter/Php/Serialized/FileRepository.php
Expand Up @@ -17,7 +17,7 @@ class FileRepository
/**
* Increment this number each time there is a B/C break in the index.
*/
private const VERSION = 1;
private const VERSION = 2;

/**
* Flush to the filesystem after BATCH_SIZE updates
Expand Down
Expand Up @@ -68,7 +68,13 @@ protected function getClassLikeRecord(string $type, Node $node, Index $index, Te
if (empty($name)) {
throw new CannotIndexNode(sprintf(
'Name is empty for file "%s"',
$document->uri()->path()
$document->uri()?->__toString() ?? 'unknown',
));
}
if (!$document->uri()) {
throw new CannotIndexNode(sprintf(
'Document has no URI for class "%s"',
$name
));
}

Expand All @@ -77,7 +83,7 @@ protected function getClassLikeRecord(string $type, Node $node, Index $index, Te
/** @var ClassDeclaration|InterfaceDeclaration|EnumDeclaration|TraitDeclaration $node */
$record->setStart(ByteOffset::fromInt($node->name->getStartPosition()));
$record->setEnd(ByteOffset::fromInt($node->name->getEndPosition()));
$record->setFilePath($document->uri()->path());
$record->setFilePath($document->uriOrThrow());
$record->setType($type);

return $record;
Expand Down
Expand Up @@ -25,7 +25,7 @@ public function index(Index $index, TextDocument $document, Node $node): void
if ($node->name instanceof MissingToken) {
throw new CannotIndexNode(sprintf(
'Class name is missing (maybe a reserved word) in: %s',
$document->uri()?->path() ?? '?',
$document->uri()?->__toString() ?? '?',
));
}
$record = $this->getClassLikeRecord(ClassRecord::TYPE_CLASS, $node, $index, $document);
Expand Down
Expand Up @@ -31,7 +31,7 @@ public function canIndex(Node $node): bool

public function beforeParse(Index $index, TextDocument $document): void
{
$fileRecord = $index->get(FileRecord::fromPath($document->uri()->path()));
$fileRecord = $index->get(FileRecord::fromPath($document->uriOrThrow()->__toString()));
assert($fileRecord instanceof FileRecord);

foreach ($fileRecord->references() as $outgoingReference) {
Expand Down Expand Up @@ -67,11 +67,11 @@ public function index(Index $index, TextDocument $document, Node $node): void

$targetRecord = $index->get(ClassRecord::fromName($name));
assert($targetRecord instanceof ClassRecord);
$targetRecord->addReference($document->uri()->path());
$targetRecord->addReference($document->uriOrThrow()->__toString());

$index->write($targetRecord);

$fileRecord = $index->get(FileRecord::fromPath($document->uri()->path()));
$fileRecord = $index->get(FileRecord::fromPath($document->uriOrThrow()->__toString()));
assert($fileRecord instanceof FileRecord);
$reference = new RecordReference(
ClassRecord::RECORD_TYPE,
Expand Down
Expand Up @@ -69,7 +69,7 @@ private function fromConstDeclaration(Node $node, Index $index, TextDocument $do
assert($record instanceof ConstantRecord);
$record->setStart(ByteOffset::fromInt($node->getStartPosition()));
$record->setEnd(ByteOffset::fromInt($node->getEndPosition()));
$record->setFilePath($document->uri()->path());
$record->setFilePath($document->uriOrThrow());
$index->write($record);
}
}
Expand All @@ -95,7 +95,7 @@ private function fromDefine(CallExpression $node, Index $index, TextDocument $do
assert($record instanceof ConstantRecord);
$record->setStart(ByteOffset::fromInt($node->getStartPosition()));
$record->setEnd(ByteOffset::fromInt($node->getEndPosition()));
$record->setFilePath($document->uri()->path());
$record->setFilePath($document->uriOrThrow());
$index->write($record);

// Return after the first argument, because we only need the name of the constant.
Expand Down
Expand Up @@ -23,7 +23,7 @@ public function index(Index $index, TextDocument $document, Node $node): void
if ($node->name instanceof MissingToken) {
throw new CannotIndexNode(sprintf(
'Class name is missing (maybe a reserved word) in: %s',
$document->uri()?->path() ?? '?',
$document->uri()?->__toString() ?? '?',
));
}
$record = $this->getClassLikeRecord(ClassRecord::TYPE_ENUM, $node, $index, $document);
Expand Down
Expand Up @@ -24,7 +24,7 @@ public function index(Index $index, TextDocument $document, Node $node): void
assert($record instanceof FunctionRecord);
$record->setStart(ByteOffset::fromInt($node->getStartPosition()));
$record->setEnd(ByteOffset::fromInt($node->getEndPosition()));
$record->setFilePath($document->uri()->path());
$record->setFilePath($document->uriOrThrow());
$index->write($record);
}

Expand Down
Expand Up @@ -20,7 +20,7 @@ public function canIndex(Node $node): bool

public function beforeParse(Index $index, TextDocument $document): void
{
$fileRecord = $index->get(FileRecord::fromPath($document->uri()->path()));
$fileRecord = $index->get(FileRecord::fromPath($document->uriOrThrow()->__toString()));
assert($fileRecord instanceof FileRecord);

foreach ($fileRecord->references() as $outgoingReference) {
Expand Down Expand Up @@ -48,10 +48,10 @@ public function index(Index $index, TextDocument $document, Node $node): void

$targetRecord = $index->get(FunctionRecord::fromName($name));
assert($targetRecord instanceof FunctionRecord);
$targetRecord->addReference($document->uri()->path());
$targetRecord->addReference($document->uriOrThrow());
$index->write($targetRecord);

$fileRecord = $index->get(FileRecord::fromPath($document->uri()->path()));
$fileRecord = $index->get(FileRecord::fromPath($document->uriOrThrow()->__toString()));
assert($fileRecord instanceof FileRecord);

$fileRecord->addReference(
Expand Down
Expand Up @@ -23,7 +23,7 @@ public function index(Index $index, TextDocument $document, Node $node): void
if ($node->name instanceof MissingToken) {
throw new CannotIndexNode(sprintf(
'Class name is missing (maybe a reserved word) in: %s',
$document->uri()?->path() ?? '?',
$document->uri()?->__toString() ?? '?',
));
}
$record = $this->getClassLikeRecord(ClassRecord::TYPE_INTERFACE, $node, $index, $document);
Expand Down
6 changes: 3 additions & 3 deletions lib/Indexer/Adapter/Tolerant/Indexer/MemberIndexer.php
Expand Up @@ -27,7 +27,7 @@ public function canIndex(Node $node): bool

public function beforeParse(Index $index, TextDocument $document): void
{
$fileRecord = $index->get(FileRecord::fromPath($document->uri()->path()));
$fileRecord = $index->get(FileRecord::fromPath($document->uriOrThrow()->__toString()));
assert($fileRecord instanceof FileRecord);

foreach ($fileRecord->references() as $outgoingReference) {
Expand Down Expand Up @@ -181,10 +181,10 @@ private function writeIndex(
): void {
$record = $index->get(MemberRecord::fromMemberReference(MemberReference::create($memberType, $containerFqn, $memberName)));
assert($record instanceof MemberRecord);
$record->addReference($document->uri()->path());
$record->addReference($document->uriOrThrow()->__toString());
$index->write($record);

$fileRecord = $index->get(FileRecord::fromPath($document->uri()->path()));
$fileRecord = $index->get(FileRecord::fromPath($document->uriOrThrow()->__toString()));
assert($fileRecord instanceof FileRecord);
$fileRecord->addReference(
RecordReference::fromRecordAndOffsetAndContainerType($record, $offsetStart, $offsetEnd, $containerFqn)
Expand Down
Expand Up @@ -23,7 +23,7 @@ public function index(Index $index, TextDocument $document, Node $node): void
if ($node->name instanceof MissingToken) {
throw new CannotIndexNode(sprintf(
'Class name is missing (maybe a reserved word) in: %s',
$document->uri()?->path() ?? '?',
$document->uri()?->__toString() ?? '?',
));
}
$record = $this->getClassLikeRecord(ClassRecord::TYPE_TRAIT, $node, $index, $document);
Expand Down
4 changes: 2 additions & 2 deletions lib/Indexer/Adapter/Tolerant/TolerantIndexBuilder.php
Expand Up @@ -63,7 +63,7 @@ public function index(TextDocument $document): void
$indexer->beforeParse($this->index, $document);
}

$node = $this->parser->parseSourceFile($document->__toString(), $document->uri()->path());
$node = $this->parser->parseSourceFile($document->__toString(), $document->uri()->__toString());
$this->indexNode($document, $node);
}

Expand All @@ -83,7 +83,7 @@ private function indexNode(TextDocument $document, Node $node): void
$this->logger->warning(sprintf(
'Cannot index node of class "%s" in file "%s": %s',
get_class($node),
$document->uri()->__toString(),
$document->uri()?->__toString() ?? 'unknown',
$cannotIndexNode->getMessage()
));
}
Expand Down
5 changes: 3 additions & 2 deletions lib/Indexer/Extension/IndexerExtension.php
Expand Up @@ -66,7 +66,7 @@ class IndexerExtension implements Extension
public const PARAM_IMPLEMENTATIONS_DEEP_REFERENCES = 'indexer.implementation_finder.deep';
public const PARAM_STUB_PATHS = 'indexer.stub_paths';
public const PARAM_SUPPORTED_EXTENSIONS = 'indexer.supported_extensions';
const TAG_WATCHER = 'indexer.watcher';
public const TAG_WATCHER = 'indexer.watcher';
private const SERVICE_INDEXER_EXCLUDE_PATTERNS = 'indexer.exclude_patterns';
private const SERVICE_INDEXER_INCLUDE_PATTERNS = 'indexer.include_patterns';
private const PARAM_PROJECT_ROOT = 'indexer.project_root';
Expand All @@ -79,6 +79,7 @@ public function configure(Resolver $schema): void
self::PARAM_INDEX_PATH => '%cache%/index/%project_id%',
self::PARAM_INCLUDE_PATTERNS => [
'/**/*.php',
'/**/*.phar',
],
self::PARAM_EXCLUDE_PATTERNS => [
'/vendor/**/Tests/**/*',
Expand All @@ -92,7 +93,7 @@ public function configure(Resolver $schema): void
self::PARAM_PROJECT_ROOT => '%project_root%',
self::PARAM_REFERENCES_DEEP_REFERENCES => true,
self::PARAM_IMPLEMENTATIONS_DEEP_REFERENCES => true,
self::PARAM_SUPPORTED_EXTENSIONS => ['php'],
self::PARAM_SUPPORTED_EXTENSIONS => ['php', 'phar'],
]);
$schema->setDescriptions([
self::PARAM_ENABLED_WATCHERS => 'List of allowed watchers. The first watcher that supports the current system will be used',
Expand Down
3 changes: 2 additions & 1 deletion lib/Indexer/IndexAgentBuilder.php
Expand Up @@ -45,6 +45,7 @@ final class IndexAgentBuilder
*/
private array $includePatterns = [
'/**/*.php',
'/**/*.phar',
];

/**
Expand All @@ -68,7 +69,7 @@ final class IndexAgentBuilder
/**
* @var list<string>
*/
private array $supportedExtensions = ['php'];
private array $supportedExtensions = ['php', 'phar'];

private LoggerInterface $logger;

Expand Down
34 changes: 34 additions & 0 deletions lib/Indexer/Model/IndexJob.php
Expand Up @@ -2,8 +2,12 @@

namespace Phpactor\Indexer\Model;

use FilesystemIterator;
use Generator;
use Phar;
use PharFileInfo;
use Phpactor\TextDocument\TextDocumentBuilder;
use RecursiveIteratorIterator;
use SplFileInfo;

class IndexJob
Expand All @@ -17,8 +21,17 @@ public function __construct(private IndexBuilder $indexBuilder, private FileList
*/
public function generator(): Generator
{

foreach ($this->fileList as $fileInfo) {
assert($fileInfo instanceof SplFileInfo);

// TODO: could refactor this to iterate the PHAR in the file list provider.
if ($fileInfo->getExtension() === 'phar') {
$phar = new Phar($fileInfo->getPathname(), FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::KEY_AS_FILENAME);
yield from $this->indexPharFile($phar);
continue;
}

$contents = @file_get_contents($fileInfo->getPathname());

if (false === $contents) {
Expand All @@ -42,4 +55,25 @@ public function size(): int
{
return $this->fileList->count();
}
/**
* @return Generator<string>
*/
private function indexPharFile(Phar $phar): Generator
{
$iterator = new RecursiveIteratorIterator($phar);
/** @var PharFileInfo $file */
foreach ($iterator as $file) {
if (!$file->isFile()) {
continue;
}
if ($file->getExtension() !== 'php') {
continue;
}

$this->indexBuilder->index(
TextDocumentBuilder::fromUri($file->getPathname())->build()
);
yield $file->getPathname();
}
}
}
Expand Up @@ -18,7 +18,14 @@ public function isSatisfiedBy(Record $record): bool
if (!$record instanceof HasPath) {
return false;
}
$path = $record->filePath();
if (!$path) {
return false;
}
if ($pos = strpos($path, ':///')) {
$path = substr($path, $pos + 3);
}

return str_starts_with($record->filePath() ?? '', $this->prefix);
return str_starts_with($path, $this->prefix);
}
}
4 changes: 2 additions & 2 deletions lib/Indexer/Model/Record/FileRecord.php
Expand Up @@ -18,9 +18,9 @@ class FileRecord implements HasPath, Record
*/
private array $references = [];

public function __construct(string $filePath)
private function __construct(string $filePath)
{
$this->setFilePath($filePath);
$this->filePath = $filePath;
}

public function __wakeup(): void
Expand Down
7 changes: 6 additions & 1 deletion lib/Indexer/Model/Record/HasPath.php
Expand Up @@ -2,12 +2,17 @@

namespace Phpactor\Indexer\Model\Record;

use Phpactor\TextDocument\TextDocumentUri;

interface HasPath
{
/**
* @return $this
*/
public function setFilePath(string $filePath);
public function setFilePath(TextDocumentUri $filePath);

/**
* Rename to URI
*/
public function filePath(): ?string;
}
6 changes: 4 additions & 2 deletions lib/Indexer/Model/Record/HasPathTrait.php
Expand Up @@ -2,13 +2,15 @@

namespace Phpactor\Indexer\Model\Record;

use Phpactor\TextDocument\TextDocumentUri;

trait HasPathTrait
{
protected ?string $filePath = null;

public function setFilePath(string $filePath): self
public function setFilePath(TextDocumentUri $uri): self
{
$this->filePath = $filePath;
$this->filePath = $uri->__toString();
return $this;
}

Expand Down