mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Collect and scan files included by the autoloaders (#3183)
Refs vimeo/psalm#2861
This commit is contained in:
parent
b8c4abf08b
commit
931d35a703
@ -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",
|
||||
|
@ -5,10 +5,10 @@ use Composer\Semver\Semver;
|
||||
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;
|
||||
@ -46,6 +46,7 @@ use Psalm\Exception\ConfigException;
|
||||
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;
|
||||
@ -568,6 +569,9 @@ class Config
|
||||
*/
|
||||
public $max_string_length = 1000;
|
||||
|
||||
/** @var ?IncludeCollector */
|
||||
private $include_collector;
|
||||
|
||||
/**
|
||||
* @var TaintAnalysisFileFilter|null
|
||||
*/
|
||||
@ -1066,18 +1070,6 @@ class Config
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $autoloader_path
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @psalm-suppress UnresolvableInclude
|
||||
*/
|
||||
private function requireAutoloader($autoloader_path)
|
||||
{
|
||||
require_once($autoloader_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
@ -1870,6 +1862,11 @@ class Config
|
||||
}
|
||||
}
|
||||
|
||||
public function setIncludeCollector(IncludeCollector $include_collector): void
|
||||
{
|
||||
$this->include_collector = $include_collector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*
|
||||
@ -1882,80 +1879,62 @@ class Config
|
||||
$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
|
||||
$vendor_autoload_files_path
|
||||
= $this->base_dir . DIRECTORY_SEPARATOR . 'vendor'
|
||||
. DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . 'autoload_files.php';
|
||||
|
||||
$autoload_files_files = [];
|
||||
|
||||
if ($this->autoloader) {
|
||||
$autoload_files_files[] = $this->autoloader;
|
||||
}
|
||||
|
||||
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'];
|
||||
|
||||
foreach ($composer_autoload_files as $file) {
|
||||
$file_path = realpath($this->base_dir . $file);
|
||||
|
||||
if ($file_path && file_exists($file_path)) {
|
||||
$autoload_files_files[] = $file_path;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
$codebase = $project_analyzer->getCodebase();
|
||||
|
||||
if ($autoload_files_files) {
|
||||
$codebase->register_autoload_files = true;
|
||||
|
||||
foreach ($autoload_files_files as $file_path) {
|
||||
$file_path = \str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $file_path);
|
||||
$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->include_collector->runAndCollect(
|
||||
function () {
|
||||
// do this in a separate method so scope does not leak
|
||||
/** @psalm-suppress UnresolvableInclude */
|
||||
require $this->autoloader;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$this->collectPredefinedConstants();
|
||||
$this->collectPredefinedFunctions();
|
||||
$autoload_included_files = $this->include_collector->getFilteredIncludedFiles();
|
||||
|
||||
if ($autoload_included_files) {
|
||||
$codebase->register_autoload_files = true;
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
$codebase->scanner->scanFiles($codebase->classlikes);
|
||||
|
||||
$progress->debug('Finished registering autoloaded files' . "\n");
|
||||
|
||||
$codebase->register_autoload_files = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
55
src/Psalm/Internal/IncludeCollector.php
Normal file
55
src/Psalm/Internal/IncludeCollector.php
Normal file
@ -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));
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Psalm\Config;
|
||||
use Psalm\Exception\ConfigException;
|
||||
|
||||
/**
|
||||
* @param string $current_dir
|
||||
@ -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;
|
||||
|
@ -3,6 +3,7 @@ require_once('command_functions.php');
|
||||
|
||||
use Psalm\Config;
|
||||
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
||||
use Psalm\Internal\IncludeCollector;
|
||||
|
||||
gc_disable();
|
||||
|
||||
@ -191,7 +192,14 @@ if (isset($options['r']) && is_string($options['r'])) {
|
||||
|
||||
$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');
|
||||
|
||||
@ -214,6 +222,7 @@ if (isset($options['tcp'])) {
|
||||
$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;
|
||||
|
@ -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;
|
||||
@ -84,7 +84,7 @@ if (isset($options['c']) && is_array($options['c'])) {
|
||||
}
|
||||
|
||||
if (array_key_exists('h', $options)) {
|
||||
echo <<< HELP
|
||||
echo <<<HELP
|
||||
Usage:
|
||||
psalm-refactor [options] [symbol1] into [symbol2]
|
||||
|
||||
@ -138,7 +138,13 @@ if (isset($options['r']) && is_string($options['r'])) {
|
||||
|
||||
$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();
|
||||
@ -228,6 +234,7 @@ if (!$to_refactor) {
|
||||
}
|
||||
|
||||
$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;
|
||||
|
@ -5,6 +5,7 @@ use Psalm\ErrorBaseline;
|
||||
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;
|
||||
@ -213,7 +214,14 @@ $path_to_config = get_path_to_config($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)) {
|
||||
@ -312,6 +320,8 @@ if (isset($options['i'])) {
|
||||
}
|
||||
}
|
||||
|
||||
$config->setIncludeCollector($include_collector);
|
||||
|
||||
if ($config->resolve_from_config_file) {
|
||||
$current_dir = $config->base_dir;
|
||||
chdir($current_dir);
|
||||
|
@ -4,6 +4,7 @@ require_once('command_functions.php');
|
||||
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;
|
||||
@ -185,7 +186,14 @@ if (isset($options['r']) && is_string($options['r'])) {
|
||||
|
||||
$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();
|
||||
@ -195,6 +203,7 @@ $paths_to_check = getPathsToCheck(isset($options['f']) ? $options['f'] : null);
|
||||
$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;
|
||||
|
@ -16,6 +16,7 @@ use Psalm\Codebase;
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\FileAnalyzer;
|
||||
use Psalm\Internal\IncludeCollector;
|
||||
use Psalm\Plugin\Hook\AfterCodebasePopulatedInterface;
|
||||
use Psalm\PluginRegistrationSocket;
|
||||
use Psalm\Tests\Internal\Provider;
|
||||
@ -62,6 +63,7 @@ class PluginTest extends \Psalm\Tests\TestCase
|
||||
*/
|
||||
private function getProjectAnalyzerWithConfig(Config $config)
|
||||
{
|
||||
$config->setIncludeCollector(new IncludeCollector());
|
||||
return new \Psalm\Internal\Analyzer\ProjectAnalyzer(
|
||||
$config,
|
||||
new \Psalm\Internal\Provider\Providers(
|
||||
|
@ -2,7 +2,6 @@
|
||||
namespace Psalm\Tests\EndToEnd;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
class SuicidalAutoloaderTest extends TestCase
|
||||
{
|
||||
|
@ -13,6 +13,7 @@ use function ob_start;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Config;
|
||||
use Psalm\Internal\Analyzer\FileAnalyzer;
|
||||
use Psalm\Internal\IncludeCollector;
|
||||
use Psalm\Plugin\Hook\AfterCodebasePopulatedInterface;
|
||||
use Psalm\Tests\Internal\Provider;
|
||||
use Psalm\Tests\Progress\EchoProgress;
|
||||
@ -57,6 +58,7 @@ class ProjectCheckerTest extends TestCase
|
||||
*/
|
||||
private function getProjectAnalyzerWithConfig(Config $config)
|
||||
{
|
||||
$config->setIncludeCollector(new IncludeCollector());
|
||||
return new \Psalm\Internal\Analyzer\ProjectAnalyzer(
|
||||
$config,
|
||||
new \Psalm\Internal\Provider\Providers(
|
||||
|
@ -11,6 +11,7 @@ use const DIRECTORY_SEPARATOR;
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\FileAnalyzer;
|
||||
use Psalm\Internal\IncludeCollector;
|
||||
use Psalm\Tests\Internal\Provider;
|
||||
|
||||
class StubTest extends TestCase
|
||||
@ -59,6 +60,7 @@ class StubTest extends TestCase
|
||||
);
|
||||
$project_analyzer->setPhpVersion('7.3');
|
||||
|
||||
$config->setIncludeCollector(new IncludeCollector());
|
||||
$config->visitComposerAutoloadFiles($project_analyzer, null);
|
||||
|
||||
return $project_analyzer;
|
||||
|
@ -4,6 +4,7 @@ namespace Psalm\Tests;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use function getcwd;
|
||||
use Psalm\Config;
|
||||
use Psalm\Internal\IncludeCollector;
|
||||
|
||||
class TestConfig extends Config
|
||||
{
|
||||
@ -33,6 +34,7 @@ class TestConfig extends Config
|
||||
}
|
||||
|
||||
$this->project_files = self::$cached_project_files;
|
||||
$this->setIncludeCollector(new IncludeCollector());
|
||||
|
||||
$this->collectPredefinedConstants();
|
||||
$this->collectPredefinedFunctions();
|
||||
|
@ -3,6 +3,7 @@ namespace Psalm\Tests;
|
||||
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\IncludeCollector;
|
||||
use Psalm\Tests\Internal\Provider;
|
||||
use function dirname;
|
||||
use function getcwd;
|
||||
@ -144,6 +145,7 @@ class VariadicTest extends TestCase
|
||||
);
|
||||
$project_analyzer->setPhpVersion('7.3');
|
||||
|
||||
$config->setIncludeCollector(new IncludeCollector());
|
||||
$config->visitComposerAutoloadFiles($project_analyzer, null);
|
||||
|
||||
return $project_analyzer;
|
||||
|
15
tests/fixtures/SuicidalAutoloader/autoloader.php
vendored
15
tests/fixtures/SuicidalAutoloader/autoloader.php
vendored
@ -1,5 +1,20 @@
|
||||
<?php
|
||||
|
||||
use React\Promise\PromiseInterface as ReactPromise;
|
||||
|
||||
spl_autoload_register(function (string $className) {
|
||||
$knownBadClasses = [
|
||||
ReactPromise::class, // amphp/amp
|
||||
ResourceBundle::class, // symfony/polyfill-php73
|
||||
Transliterator::class, // symfony/string
|
||||
// it's unclear why Psalm tries to autoload parent
|
||||
'parent',
|
||||
];
|
||||
|
||||
if (in_array($className, $knownBadClasses)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ex = new RuntimeException('Attempted to load ' . $className);
|
||||
echo $ex->__toString() . "\n\n" . $ex->getTraceAsString() . "\n\n";
|
||||
exit(70);
|
||||
|
Loading…
Reference in New Issue
Block a user