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

Scan files included by autoloaders #3183

Merged
merged 1 commit into from
Jul 11, 2020
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
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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