1
0
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:
Bruce Weirdan 2020-07-12 00:17:22 +03:00 committed by GitHub
parent b8c4abf08b
commit 931d35a703
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 173 additions and 82 deletions

View File

@ -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",

View File

@ -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;
}
}

View 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));
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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(

View File

@ -2,7 +2,6 @@
namespace Psalm\Tests\EndToEnd;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\Process;
class SuicidalAutoloaderTest extends TestCase
{

View File

@ -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(

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -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);