Skip to content

Commit

Permalink
Collect and scan files included by the autoloaders (#3183)
Browse files Browse the repository at this point in the history
Refs #2861
  • Loading branch information
weirdan committed Jul 11, 2020
1 parent b8c4abf commit 931d35a
Show file tree
Hide file tree
Showing 15 changed files with 166 additions and 75 deletions.
1 change: 0 additions & 1 deletion composer.json
Expand Up @@ -43,7 +43,6 @@
"amphp/amp": "^2.4.2",
"bamarni/composer-bin-plugin": "^1.2",
"brianium/paratest": "^4.0.0",
"php-coveralls/php-coveralls": "^2.2",
"phpmyadmin/sql-parser": "5.1.0",
"phpspec/prophecy": ">=1.9.0",
"phpunit/phpunit": "^7.5.16 || ^8.5 || ^9.0",
Expand Down
109 changes: 44 additions & 65 deletions src/Psalm/Config.php
Expand Up @@ -5,10 +5,10 @@
use Webmozart\PathUtil\Path;
use function array_merge;
use function array_pop;
use function array_unique;
use function class_exists;
use Composer\Autoload\ClassLoader;
use DOMDocument;
use LogicException;

use function count;
use const DIRECTORY_SEPARATOR;
Expand Down Expand Up @@ -46,6 +46,7 @@
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\FileAnalyzer;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\IncludeCollector;
use Psalm\Internal\Scanner\FileScanner;
use Psalm\Issue\ArgumentIssue;
use Psalm\Issue\ClassIssue;
Expand Down Expand Up @@ -568,6 +569,9 @@ class Config
*/
public $max_string_length = 1000;

/** @var ?IncludeCollector */
private $include_collector;

/**
* @var TaintAnalysisFileFilter|null
*/
Expand Down Expand Up @@ -1066,18 +1070,6 @@ private static function fromXmlAndPaths(string $base_dir, string $file_contents,
return $config;
}

/**
* @param string $autoloader_path
*
* @return void
*
* @psalm-suppress UnresolvableInclude
*/
private function requireAutoloader($autoloader_path)
{
require_once($autoloader_path);
}

/**
* @return $this
*/
Expand Down Expand Up @@ -1870,6 +1862,11 @@ public function collectPredefinedFunctions()
}
}

public function setIncludeCollector(IncludeCollector $include_collector): void
{
$this->include_collector = $include_collector;
}

/**
* @return void
*
Expand All @@ -1882,81 +1879,63 @@ public function visitComposerAutoloadFiles(ProjectAnalyzer $project_analyzer, Pr
$progress = new VoidProgress();
}

if (!$this->include_collector) {
throw new LogicException("IncludeCollector should be set at this point");
}

$this->collectPredefinedConstants();
$this->collectPredefinedFunctions();

$composer_json_path = $this->base_dir . 'composer.json'; // this should ideally not be hardcoded

$autoload_files_files = [];

if ($this->autoloader) {
$autoload_files_files[] = $this->autoloader;
$vendor_autoload_files_path
= $this->base_dir . DIRECTORY_SEPARATOR . 'vendor'
. DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_files.php';

if (file_exists($vendor_autoload_files_path)) {
$this->include_collector->runAndCollect(
function () use ($vendor_autoload_files_path) {
/**
* @psalm-suppress UnresolvableInclude
* @var string[]
*/
return require $vendor_autoload_files_path;
}
);
}

if (file_exists($composer_json_path)) {
if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) {
throw new \UnexpectedValueException('Invalid composer.json at ' . $composer_json_path);
}

if (isset($composer_json['autoload']['files'])) {
/** @var string[] */
$composer_autoload_files = $composer_json['autoload']['files'];
$codebase = $project_analyzer->getCodebase();

foreach ($composer_autoload_files as $file) {
$file_path = realpath($this->base_dir . $file);
if ($this->autoloader) {
// somee classes that we think are missing may not actually be missing
// as they might be autoloadable once we require the autoloader below
$codebase->classlikes->forgetMissingClassLikes();

if ($file_path && file_exists($file_path)) {
$autoload_files_files[] = $file_path;
}
$this->include_collector->runAndCollect(
function () {
// do this in a separate method so scope does not leak
/** @psalm-suppress UnresolvableInclude */
require $this->autoloader;
}
}

$vendor_autoload_files_path
= $this->base_dir . DIRECTORY_SEPARATOR . 'vendor'
. DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_files.php';

if (file_exists($vendor_autoload_files_path)) {
/**
* @var string[]
*/
$vendor_autoload_files = require $vendor_autoload_files_path;

$autoload_files_files = array_merge($autoload_files_files, $vendor_autoload_files);
}
);
}

$autoload_files_files = array_unique($autoload_files_files);
$autoload_included_files = $this->include_collector->getFilteredIncludedFiles();

$codebase = $project_analyzer->getCodebase();

if ($autoload_files_files) {
if ($autoload_included_files) {
$codebase->register_autoload_files = true;

foreach ($autoload_files_files as $file_path) {
$progress->debug('Registering autoloaded files' . "\n");
foreach ($autoload_included_files as $file_path) {
$file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path);
$progress->debug(' ' . $file_path . "\n");
$codebase->scanner->addFileToDeepScan($file_path);
}

$progress->debug('Registering autoloaded files' . "\n");

$codebase->scanner->scanFiles($codebase->classlikes);

$progress->debug('Finished registering autoloaded files' . "\n");

$codebase->register_autoload_files = false;
}

if ($this->autoloader) {
// somee classes that we think are missing may not actually be missing
// as they might be autoloadable once we require the autoloader below
$codebase->classlikes->forgetMissingClassLikes();

// do this in a separate method so scope does not leak
$this->requireAutoloader($this->autoloader);

$this->collectPredefinedConstants();
$this->collectPredefinedFunctions();
}
}

/**
Expand Down
55 changes: 55 additions & 0 deletions src/Psalm/Internal/IncludeCollector.php
@@ -0,0 +1,55 @@
<?php

namespace Psalm\Internal;

use function array_diff;
use function array_merge;
use function array_unique;
use function array_values;
use function get_included_files;
use function preg_grep;

use const PREG_GREP_INVERT;

/**
* Include collector
*
* Used to execute code that may cause file inclusions, and report what files have been included
* NOTE: dependencies of this class should be kept at minimum, as it's used before autoloader is
* registered.
*/
final class IncludeCollector
{
/** @var list<string> */
private $included_files = [];

/**
* @template T
* @param callable():T $f
* @return T
*/
public function runAndCollect(callable $f)
{
$before = get_included_files();
$ret = $f();
$after = get_included_files();

$included = array_diff($after, $before);

$this->included_files = array_values(array_unique(array_merge($this->included_files, $included)));

return $ret;
}

/** @return list<string> */
public function getIncludedFiles(): array
{
return $this->included_files;
}

/** @return list<string> */
public function getFilteredIncludedFiles(): array
{
return array_values(preg_grep('@^phar://@', $this->getIncludedFiles(), PREG_GREP_INVERT));
}
}
3 changes: 1 addition & 2 deletions src/command_functions.php
Expand Up @@ -2,7 +2,6 @@

use Composer\Autoload\ClassLoader;
use Psalm\Config;
use Psalm\Exception\ConfigException;

/**
* @param string $current_dir
Expand Down Expand Up @@ -102,7 +101,7 @@ function requireAutoloaders($current_dir, $has_explicit_root, $vendor_dir)
exit(1);
}

define('PSALM_VERSION', \PackageVersions\Versions::getVersion('vimeo/psalm'));
define('PSALM_VERSION', (string)\PackageVersions\Versions::getVersion('vimeo/psalm'));
define('PHP_PARSER_VERSION', \PackageVersions\Versions::getVersion('nikic/php-parser'));

return $first_autoloader;
Expand Down
11 changes: 10 additions & 1 deletion src/psalm-language-server.php
Expand Up @@ -3,6 +3,7 @@

use Psalm\Config;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\IncludeCollector;

gc_disable();

Expand Down Expand Up @@ -191,7 +192,14 @@ function ($arg) use ($valid_long_options, $valid_short_options) {

$vendor_dir = getVendorDir($current_dir);

$first_autoloader = requireAutoloaders($current_dir, isset($options['r']), $vendor_dir);
require_once __DIR__ . '/Psalm/Internal/IncludeCollector.php';
$include_collector = new IncludeCollector();

$first_autoloader = $include_collector->runAndCollect(
function () use ($current_dir, $options, $vendor_dir) {
return requireAutoloaders($current_dir, isset($options['r']), $vendor_dir);
}
);

$ini_handler = new \Psalm\Internal\Fork\PsalmRestarter('PSALM');

Expand All @@ -214,6 +222,7 @@ function ($arg) use ($valid_long_options, $valid_short_options) {
$find_dead_code = isset($options['find-dead-code']);

$config = initialiseConfig($path_to_config, $current_dir, \Psalm\Report::TYPE_CONSOLE, $first_autoloader);
$config->setIncludeCollector($include_collector);

if ($config->resolve_from_config_file) {
$current_dir = $config->base_dir;
Expand Down
13 changes: 10 additions & 3 deletions src/psalm-refactor.php
Expand Up @@ -2,7 +2,7 @@
require_once('command_functions.php');

use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Config;
use Psalm\Internal\IncludeCollector;
use Psalm\IssueBuffer;
use Psalm\Progress\DebugProgress;
use Psalm\Progress\DefaultProgress;
Expand Down Expand Up @@ -84,7 +84,7 @@ function ($arg) use ($valid_long_options, $valid_short_options) {
}

if (array_key_exists('h', $options)) {
echo <<< HELP
echo <<<HELP
Usage:
psalm-refactor [options] [symbol1] into [symbol2]
Expand Down Expand Up @@ -138,7 +138,13 @@ function ($arg) use ($valid_long_options, $valid_short_options) {

$vendor_dir = getVendorDir($current_dir);

$first_autoloader = requireAutoloaders($current_dir, isset($options['r']), $vendor_dir);
require_once __DIR__ . '/Psalm/Internal/IncludeCollector.php';
$include_collector = new IncludeCollector();
$first_autoloader = $include_collector->runAndCollect(
function () use ($current_dir, $options, $vendor_dir) {
return requireAutoloaders($current_dir, isset($options['r']), $vendor_dir);
}
);

// If Xdebug is enabled, restart without it
(new \Composer\XdebugHandler\XdebugHandler('PSALTER'))->check();
Expand Down Expand Up @@ -228,6 +234,7 @@ function ($arg) use ($valid_long_options, $valid_short_options) {
}

$config = initialiseConfig($path_to_config, $current_dir, \Psalm\Report::TYPE_CONSOLE, $first_autoloader);
$config->setIncludeCollector($include_collector);

if ($config->resolve_from_config_file) {
$current_dir = $config->base_dir;
Expand Down
12 changes: 11 additions & 1 deletion src/psalm.php
Expand Up @@ -5,6 +5,7 @@
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\Provider;
use Psalm\Config;
use Psalm\Internal\IncludeCollector;
use Psalm\IssueBuffer;
use Psalm\Progress\DebugProgress;
use Psalm\Progress\DefaultProgress;
Expand Down Expand Up @@ -213,7 +214,14 @@ function ($arg) use ($valid_long_options, $valid_short_options) {

$vendor_dir = getVendorDir($current_dir);

$first_autoloader = requireAutoloaders($current_dir, isset($options['r']), $vendor_dir);
require_once __DIR__ . '/' . 'Psalm/Internal/IncludeCollector.php';

$include_collector = new IncludeCollector();
$first_autoloader = $include_collector->runAndCollect(
function () use ($current_dir, $options, $vendor_dir) {
return requireAutoloaders($current_dir, isset($options['r']), $vendor_dir);
}
);


if (array_key_exists('v', $options)) {
Expand Down Expand Up @@ -312,6 +320,8 @@ function ($arg) {
}
}

$config->setIncludeCollector($include_collector);

if ($config->resolve_from_config_file) {
$current_dir = $config->base_dir;
chdir($current_dir);
Expand Down
11 changes: 10 additions & 1 deletion src/psalter.php
Expand Up @@ -4,6 +4,7 @@
use Psalm\DocComment;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Config;
use Psalm\Internal\IncludeCollector;
use Psalm\IssueBuffer;
use Psalm\Progress\DebugProgress;
use Psalm\Progress\DefaultProgress;
Expand Down Expand Up @@ -185,7 +186,14 @@ function ($arg) use ($valid_long_options, $valid_short_options) {

$vendor_dir = getVendorDir($current_dir);

$first_autoloader = requireAutoloaders($current_dir, isset($options['r']), $vendor_dir);
require_once __DIR__ . '/Psalm/Internal/IncludeCollector.php';
$include_collector = new IncludeCollector();
$first_autoloader = $include_collector->runAndCollect(
function () use ($current_dir, $options, $vendor_dir) {
return requireAutoloaders($current_dir, isset($options['r']), $vendor_dir);
}
);


// If Xdebug is enabled, restart without it
(new \Composer\XdebugHandler\XdebugHandler('PSALTER'))->check();
Expand All @@ -195,6 +203,7 @@ function ($arg) use ($valid_long_options, $valid_short_options) {
$path_to_config = get_path_to_config($options);

$config = initialiseConfig($path_to_config, $current_dir, \Psalm\Report::TYPE_CONSOLE, $first_autoloader);
$config->setIncludeCollector($include_collector);

if ($config->resolve_from_config_file) {
$current_dir = $config->base_dir;
Expand Down

0 comments on commit 931d35a

Please sign in to comment.