Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion build/spl-autoload-functions-php-8.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ parameters:
ignoreErrors:
-
message: "#^PHPDoc tag @var with type list\\<callable\\(string\\): void\\>\\|false is not subtype of native type list\\<callable\\(string\\): void\\>\\.$#"
count: 2
count: 1
path: ../src/Command/CommandHelper.php
-
message: "#^PHPDoc tag @var with type list\\<callable\\(string\\): void\\>\\|false is not subtype of native type list\\<callable\\(string\\): void\\>\\.$#"
count: 2
path: ../src/Command/BootstrapFilesRunner.php
10 changes: 9 additions & 1 deletion src/Command/AnalyseApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ final class AnalyseApplication
{

public function __construct(
private BootstrapFilesRunner $bootstrapFilesRunner,
private AnalyserRunner $analyserRunner,
private AnalyserResultFinalizer $analyserResultFinalizer,
private StubValidator $stubValidator,
Expand All @@ -50,6 +51,7 @@ public function __construct(
/**
* @param string[] $files
* @param mixed[]|null $projectConfigArray
* @throws InceptionNotSuccessfulException
*/
public function analyse(
array $files,
Expand Down Expand Up @@ -214,6 +216,7 @@ private function mapCollectedData(array $collectedData): array
/**
* @param string[] $files
* @param string[] $allAnalysedFiles
* @throws InceptionNotSuccessfulException
*/
private function runAnalyser(
array $files,
Expand All @@ -230,6 +233,11 @@ private function runAnalyser(
$filesCount = count($files);
$allAnalysedFilesCount = count($allAnalysedFiles);
if ($filesCount === 0) {
// nothing to analyse, but the deferred bootstrapFiles still must
// run in the main thread: the phases that follow may reflect
// analysed code (stub validation, collector rules from the result
// cache)
$this->bootstrapFilesRunner->run($errorOutput, $debug);
$errorOutput->getStyle()->progressStart($allAnalysedFilesCount);
$errorOutput->getStyle()->progressAdvance($allAnalysedFilesCount);
$errorOutput->getStyle()->progressFinish();
Expand Down Expand Up @@ -296,7 +304,7 @@ private function runAnalyser(
}
}

$analyserResult = $this->analyserRunner->runAnalyser($files, $allAnalysedFiles, $preFileCallback, $postFileCallback, $debug, true, $projectConfigFile, $tmpFile, $insteadOfFile, $input);
$analyserResult = $this->analyserRunner->runAnalyser($files, $allAnalysedFiles, $preFileCallback, $postFileCallback, $debug, true, $projectConfigFile, $tmpFile, $insteadOfFile, $input, $errorOutput);

if (!$debug) {
$errorOutput->getStyle()->progressFinish();
Expand Down
1 change: 1 addition & 0 deletions src/Command/AnalyseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$tmpFile,
$insteadOfFile,
true,
deferBootstrapFiles: true,
);
} catch (InceptionNotSuccessfulException $e) {
return 1;
Expand Down
15 changes: 15 additions & 0 deletions src/Command/AnalyserRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function __construct(
private Analyser $analyser,
private ParallelAnalyser $parallelAnalyser,
private CpuCoreCounter $cpuCoreCounter,
private BootstrapFilesRunner $bootstrapFilesRunner,
)
{
}
Expand All @@ -39,6 +40,7 @@ public function __construct(
* @param string[] $allAnalysedFiles
* @param Closure(string $file): void|null $preFileCallback
* @param Closure(int, list<string>=): void|null $postFileCallback
* @throws InceptionNotSuccessfulException
*/
public function runAnalyser(
array $files,
Expand All @@ -51,6 +53,7 @@ public function runAnalyser(
?string $tmpFile,
?string $insteadOfFile,
InputInterface $input,
Output $errorOutput,
): AnalyserResult
{
$filesCount = count($files);
Expand Down Expand Up @@ -93,10 +96,22 @@ public function runAnalyser(
if ($result === null) {
throw new ShouldNotHappenException();
}
// the parallel analysis is over and no more workers fork - the
// main thread runs the deferred bootstrapFiles now, before the
// phases that may reflect analysed code (stub validation,
// collector rules)
$this->bootstrapFilesRunner->run($errorOutput, $debug);

return $result;
}
}

// every path below analyses in-process - including the fall-throughs
// from the parallel branch above (no main script, zero-process
// schedule) - so the main thread runs the deferred bootstrapFiles
// first
$this->bootstrapFilesRunner->run($errorOutput, $debug);

return $this->analyser->analyse(
$this->switchTmpFile($files, $insteadOfFile, $tmpFile),
$preFileCallback,
Expand Down
88 changes: 88 additions & 0 deletions src/Command/BootstrapFilesRunner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php declare(strict_types = 1);

namespace PHPStan\Command;

use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\DependencyInjection\Container;
use function array_merge;
use function PHPStan\collectNewAutoloadFunctions;
use function spl_autoload_functions;

/**
* Executes the configured bootstrapFiles, once per process.
*
* The analyse flow calls run() at exactly the places that need the files
* executed - resources they open (an objectManagerLoader's database
* connection, a query reflector's PDO) must not be inherited by forked
* children:
*
* - the main thread right before an in-process analysis (AnalyserRunner),
* - every worker, spawned or forked (WorkerRunner),
* - the main thread after the workers of a parallel analysis
* (AnalyserRunner), and after a fully cached run (AnalyseApplication) -
* before the phases that may reflect analysed code.
*
* Every other command runs them eagerly in CommandHelper::begin(). The
* once-per-process latch exists for a worker forked from a parent that
* already ran the files (the fixer flow runs them eagerly before its
* repeated analysis rounds): such a worker inherits that execution and
* run() must not repeat it.
*/
#[AutowiredService]
final class BootstrapFilesRunner
{

private bool $hasRun = false;

public function __construct(private Container $container)
{
}

/**
* @throws InceptionNotSuccessfulException
*/
public function run(Output $errorOutput, bool $debugEnabled): void
{
if ($this->hasRun) {
return;
}
$this->hasRun = true;

/** @var list<callable(string): void>|false $autoloadFunctionsBefore */
$autoloadFunctionsBefore = spl_autoload_functions();

foreach ($this->container->getParameter('bootstrapFiles') as $bootstrapFile) {
CommandHelper::executeBootstrapFile($bootstrapFile, $this->container, $errorOutput, $debugEnabled);
}

self::mergeNewAutoloadFunctions($autoloadFunctionsBefore);
}

/**
* Merges autoloaders registered since $autoloadFunctionsBefore into the
* globals the BetterReflection source locators consult lazily (see
* autoloadFunctions.php) - late merging in a deferred or forked run is
* picked up by the next reflection ask.
*
* @param list<callable(string): void>|false $autoloadFunctionsBefore
*/
public static function mergeNewAutoloadFunctions(array|false $autoloadFunctionsBefore): void
{
/** @var list<callable(string): void>|false $autoloadFunctionsAfter */
$autoloadFunctionsAfter = spl_autoload_functions();
if ($autoloadFunctionsBefore === false || $autoloadFunctionsAfter === false) {
return;
}

$collectedAutoloadFunctions = collectNewAutoloadFunctions($autoloadFunctionsBefore, $autoloadFunctionsAfter);
$GLOBALS['__phpstanAutoloadFunctions'] = array_merge(
$GLOBALS['__phpstanAutoloadFunctions'] ?? [],
$collectedAutoloadFunctions['appended'],
);
$GLOBALS['__phpstanAutoloadFunctionsPrependedToComposer'] = array_merge(
$GLOBALS['__phpstanAutoloadFunctionsPrependedToComposer'] ?? [],
$collectedAutoloadFunctions['prepended'],
);
}

}
41 changes: 21 additions & 20 deletions src/Command/CommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
use function array_filter;
use function array_key_exists;
use function array_map;
use function array_merge;
use function array_values;
use function class_exists;
use function count;
Expand All @@ -57,7 +56,6 @@
use function is_file;
use function is_readable;
use function is_string;
use function PHPStan\collectNewAutoloadFunctions;
use function register_shutdown_function;
use function spl_autoload_functions;
use function sprintf;
Expand Down Expand Up @@ -96,6 +94,7 @@ public static function begin(
?string $singleReflectionFile,
?string $singleReflectionInsteadOfFile,
bool $cleanupContainerCache,
bool $deferBootstrapFiles = false,
): InceptionResult
{
$stdOutput = new SymfonyOutput($output, new SymfonyStyle(new ErrorsConsoleStyle($input, $output)));
Expand Down Expand Up @@ -520,23 +519,25 @@ public static function begin(
$defaultLevelUsed = false;
}

foreach ($container->getParameter('bootstrapFiles') as $bootstrapFileFromArray) {
self::executeBootstrapFile($bootstrapFileFromArray, $container, $errorOutput, $debugEnabled);
}

/** @var list<callable(string): void>|false $autoloadFunctionsAfter */
$autoloadFunctionsAfter = spl_autoload_functions();

if ($autoloadFunctionsBefore !== false && $autoloadFunctionsAfter !== false) {
$collectedAutoloadFunctions = collectNewAutoloadFunctions($autoloadFunctionsBefore, $autoloadFunctionsAfter);
$GLOBALS['__phpstanAutoloadFunctions'] = array_merge(
$GLOBALS['__phpstanAutoloadFunctions'] ?? [],
$collectedAutoloadFunctions['appended'],
);
$GLOBALS['__phpstanAutoloadFunctionsPrependedToComposer'] = array_merge(
$GLOBALS['__phpstanAutoloadFunctionsPrependedToComposer'] ?? [],
$collectedAutoloadFunctions['prepended'],
);
// merges autoloaders registered since the top of begin() - the
// --autoload-file require, anything container creation pulled in. This
// cannot wait for the bootstrapFiles run: when that run is deferred,
// the parent analyses long before it happens, and the source locators
// must see these autoloaders throughout. It also cannot fold into
// BootstrapFilesRunner::run() - by the time run() snapshots its own
// before-list these are already registered, so its diff would exclude
// them. run() brackets only the bootstrap files themselves, in
// whichever process executes them.
BootstrapFilesRunner::mergeNewAutoloadFunctions($autoloadFunctionsBefore);

// the analyse flow defers bootstrapFiles to the exact places that need
// them: the main thread right before an in-process analysis, every
// worker (spawned or forked - WorkerRunner), and the main thread after
// the workers of a parallel analysis (AnalyserRunner) - resources the
// files open (database connections!) must not be inherited by forked
// children. Every other command runs them here.
if (!$deferBootstrapFiles) {
$container->getByType(BootstrapFilesRunner::class)->run($errorOutput, $debugEnabled);
}

if (PHP_VERSION_ID >= 80000) {
Expand Down Expand Up @@ -627,7 +628,7 @@ public static function begin(
/**
* @throws InceptionNotSuccessfulException
*/
private static function executeBootstrapFile(
public static function executeBootstrapFile(
string $file,
Container $container,
Output $errorOutput,
Expand Down
22 changes: 14 additions & 8 deletions src/Command/WorkerCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$tmpFile,
$insteadOfFile,
false,
deferBootstrapFiles: true,
);
} catch (InceptionNotSuccessfulException $e) {
return 1;
Expand Down Expand Up @@ -143,14 +144,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
// child can reuse it without re-booting (see ParallelAnalyser).
$workerRunner = $container->getByType(WorkerRunner::class);

return $workerRunner->run(
$output,
$analysedFiles,
(int) $port,
$identifier,
$tmpFile,
$insteadOfFile,
);
try {
return $workerRunner->run(
$output,
$analysedFiles,
(int) $port,
$identifier,
$tmpFile,
$insteadOfFile,
);
} catch (InceptionNotSuccessfulException) {
// a deferred bootstrap file failed - its error is already printed
return 1;
}
}

}
23 changes: 15 additions & 8 deletions src/Parallel/ForkedProcess.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Parallel;

use PHPStan\Command\InceptionNotSuccessfulException;
use PHPStan\ShouldNotHappenException;
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;
Expand Down Expand Up @@ -88,14 +89,20 @@ public function start(callable $onData, callable $onError, callable $onExit): vo
// the worker on its own fresh event loop and never return.
$this->server->close();
$output = new StreamOutput($this->stdOut);
$exitCode = $this->workerRunner->run(
$output,
$this->analysedFiles,
$this->serverPort,
$this->identifier,
$this->tmpFile,
$this->insteadOfFile,
);
try {
$exitCode = $this->workerRunner->run(
$output,
$this->analysedFiles,
$this->serverPort,
$this->identifier,
$this->tmpFile,
$this->insteadOfFile,
);
} catch (InceptionNotSuccessfulException) {
// a deferred bootstrap file failed - its error is already
// printed to the child's collected stdout
exit(1);
}
exit($exitCode);
}

Expand Down
Loading
Loading