Skip to content

Commit

Permalink
Detect duplicate included files in ContainerFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Oct 20, 2022
1 parent 91496e1 commit f15cd6d
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 105 deletions.
1 change: 1 addition & 0 deletions build/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ parameters:
- 'Symfony\Component\Finder\Exception\DirectoryNotFoundException'
- 'InvalidArgumentException'
- 'PHPStan\DependencyInjection\ParameterNotFoundException'
- 'PHPStan\DependencyInjection\DuplicateIncludedFilesException'
- 'PHPStan\Analyser\UndefinedVariableException'
- 'RuntimeException'
- 'Nette\Neon\Exception'
Expand Down
119 changes: 14 additions & 105 deletions src/Command/CommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Closure;
use Composer\XdebugHandler\XdebugHandler;
use Nette\DI\Config\Adapters\PhpAdapter;
use Nette\DI\Helpers;
use Nette\DI\InvalidConfigurationException;
use Nette\DI\ServiceCreationException;
Expand All @@ -13,14 +12,13 @@
use Nette\Schema\ValidationException;
use Nette\Utils\AssertionException;
use Nette\Utils\Strings;
use Nette\Utils\Validators;
use PHPStan\Command\Symfony\SymfonyOutput;
use PHPStan\Command\Symfony\SymfonyStyle;
use PHPStan\DependencyInjection\Container;
use PHPStan\DependencyInjection\ContainerFactory;
use PHPStan\DependencyInjection\DuplicateIncludedFilesException;
use PHPStan\DependencyInjection\InvalidIgnoredErrorPatternsException;
use PHPStan\DependencyInjection\LoaderFactory;
use PHPStan\DependencyInjection\NeonAdapter;
use PHPStan\ExtensionInstaller\GeneratedConfig;
use PHPStan\File\FileFinder;
use PHPStan\File\FileHelper;
Expand All @@ -30,11 +28,8 @@
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use function array_diff_key;
use function array_key_exists;
use function array_map;
use function array_merge;
use function array_unique;
use function class_exists;
use function count;
use function dirname;
Expand All @@ -53,7 +48,6 @@
use function register_shutdown_function;
use function spl_autoload_functions;
use function sprintf;
use function str_ends_with;
use function str_repeat;
use function strpos;
use function sys_get_temp_dir;
Expand Down Expand Up @@ -270,18 +264,6 @@ public static function begin(
$additionalConfigFiles[] = $projectConfigFile;
}

$loaderParameters = [
'rootDir' => $containerFactory->getRootDirectory(),
'currentWorkingDirectory' => $containerFactory->getCurrentWorkingDirectory(),
];

self::detectDuplicateIncludedFiles(
$errorOutput,
$currentWorkingDirectoryFileHelper,
$additionalConfigFiles,
$loaderParameters,
);

$createDir = static function (string $path) use ($errorOutput): void {
if (!is_dir($path) && !@mkdir($path, 0777) && !is_dir($path)) {
$errorOutput->writeLineFormatted(sprintf('Cannot create a temp directory %s', $path));
Expand Down Expand Up @@ -343,6 +325,19 @@ public static function begin(
$errorOutput->writeLineFormatted(sprintf("\t\t%s: @defaultAnalysisParser", $matches['parameterName']));
$errorOutput->writeLineFormatted('');

throw new InceptionNotSuccessfulException();
} catch (DuplicateIncludedFilesException $e) {
$format = "<error>These files are included multiple times:</error>\n- %s";
if (count($e->getFiles()) === 1) {
$format = "<error>This file is included multiple times:</error>\n- %s";
}
$errorOutput->writeLineFormatted(sprintf($format, implode("\n- ", $e->getFiles())));

if (class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) {
$errorOutput->writeLineFormatted('');
$errorOutput->writeLineFormatted('It can lead to unexpected results. If you\'re using phpstan/extension-installer, make sure you have removed corresponding neon files from your project config file.');
}

throw new InceptionNotSuccessfulException();
}

Expand Down Expand Up @@ -516,90 +511,4 @@ private static function executeBootstrapFile(
}
}

/**
* @param string[] $configFiles
* @param array<string, string> $loaderParameters
* @throws InceptionNotSuccessfulException
*/
private static function detectDuplicateIncludedFiles(
Output $output,
FileHelper $fileHelper,
array $configFiles,
array $loaderParameters,
): void
{
$neonAdapter = new NeonAdapter();
$phpAdapter = new PhpAdapter();
$allConfigFiles = [];
foreach ($configFiles as $configFile) {
$allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null));
}

$normalized = array_map(static fn (string $file): string => $fileHelper->normalizePath($file), $allConfigFiles);

$deduplicated = array_unique($normalized);
if (count($normalized) <= count($deduplicated)) {
return;
}

$duplicateFiles = array_unique(array_diff_key($normalized, $deduplicated));

$format = "<error>These files are included multiple times:</error>\n- %s";
if (count($duplicateFiles) === 1) {
$format = "<error>This file is included multiple times:</error>\n- %s";
}
$output->writeLineFormatted(sprintf($format, implode("\n- ", $duplicateFiles)));

if (class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) {
$output->writeLineFormatted('');
$output->writeLineFormatted('It can lead to unexpected results. If you\'re using phpstan/extension-installer, make sure you have removed corresponding neon files from your project config file.');
}
throw new InceptionNotSuccessfulException();
}

/**
* @param array<string, string> $loaderParameters
* @return string[]
*/
private static function getConfigFiles(
FileHelper $fileHelper,
NeonAdapter $neonAdapter,
PhpAdapter $phpAdapter,
string $configFile,
array $loaderParameters,
?string $generateBaselineFile,
): array
{
if ($generateBaselineFile === $fileHelper->normalizePath($configFile)) {
return [];
}
if (!is_file($configFile) || !is_readable($configFile)) {
return [];
}

if (str_ends_with($configFile, '.php')) {
$data = $phpAdapter->load($configFile);
} else {
$data = $neonAdapter->load($configFile);
}
$allConfigFiles = [$configFile];
if (isset($data['includes'])) {
Validators::assert($data['includes'], 'list', sprintf("section 'includes' in file '%s'", $configFile));
$includes = Helpers::expand($data['includes'], $loaderParameters);
foreach ($includes as $include) {
$include = self::expandIncludedFile($include, $configFile);
$allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $include, $loaderParameters, $generateBaselineFile));
}
}

return $allConfigFiles;
}

private static function expandIncludedFile(string $includedFile, string $mainFile): string
{
return Strings::match($includedFile, '#([a-z]+:)?[/\\\\]#Ai') !== null // is absolute
? $includedFile
: dirname($mainFile) . '/' . $includedFile;
}

}
95 changes: 95 additions & 0 deletions src/DependencyInjection/ContainerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

namespace PHPStan\DependencyInjection;

use Nette\DI\Config\Adapters\PhpAdapter;
use Nette\DI\Extensions\ExtensionsExtension;
use Nette\DI\Extensions\PhpExtension;
use Nette\DI\Helpers;
use Nette\Utils\Strings;
use Nette\Utils\Validators;
use Phar;
use PhpParser\Parser;
use PHPStan\BetterReflection\BetterReflection;
Expand All @@ -17,10 +21,19 @@
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
use Symfony\Component\Finder\Finder;
use function array_diff_key;
use function array_map;
use function array_merge;
use function array_unique;
use function count;
use function dirname;
use function extension_loaded;
use function ini_get;
use function is_dir;
use function is_file;
use function is_readable;
use function sprintf;
use function str_ends_with;
use function sys_get_temp_dir;
use function time;
use function unlink;
Expand Down Expand Up @@ -71,6 +84,14 @@ public function create(
?string $singleReflectionInsteadOfFile = null,
): Container
{
$this->detectDuplicateIncludedFiles(
$additionalConfigFiles,
[
'rootDir' => $this->rootDirectory,
'currentWorkingDirectory' => $this->currentWorkingDirectory,
],
);

$configurator = new Configurator(new LoaderFactory(
$this->fileHelper,
$this->rootDirectory,
Expand Down Expand Up @@ -187,4 +208,78 @@ public function getConfigDirectory(): string
return $this->configDirectory;
}

/**
* @param string[] $configFiles
* @param array<string, string> $loaderParameters
* @throws DuplicateIncludedFilesException
*/
private function detectDuplicateIncludedFiles(
array $configFiles,
array $loaderParameters,
): void
{
$neonAdapter = new NeonAdapter();
$phpAdapter = new PhpAdapter();
$allConfigFiles = [];
foreach ($configFiles as $configFile) {
$allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($this->fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null));
}

$normalized = array_map(fn (string $file): string => $this->fileHelper->normalizePath($file), $allConfigFiles);

$deduplicated = array_unique($normalized);
if (count($normalized) <= count($deduplicated)) {
return;
}

$duplicateFiles = array_unique(array_diff_key($normalized, $deduplicated));

throw new DuplicateIncludedFilesException($duplicateFiles);
}

/**
* @param array<string, string> $loaderParameters
* @return string[]
*/
private static function getConfigFiles(
FileHelper $fileHelper,
NeonAdapter $neonAdapter,
PhpAdapter $phpAdapter,
string $configFile,
array $loaderParameters,
?string $generateBaselineFile,
): array
{
if ($generateBaselineFile === $fileHelper->normalizePath($configFile)) {
return [];
}
if (!is_file($configFile) || !is_readable($configFile)) {
return [];
}

if (str_ends_with($configFile, '.php')) {
$data = $phpAdapter->load($configFile);
} else {
$data = $neonAdapter->load($configFile);
}
$allConfigFiles = [$configFile];
if (isset($data['includes'])) {
Validators::assert($data['includes'], 'list', sprintf("section 'includes' in file '%s'", $configFile));
$includes = Helpers::expand($data['includes'], $loaderParameters);
foreach ($includes as $include) {
$include = self::expandIncludedFile($include, $configFile);
$allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $include, $loaderParameters, $generateBaselineFile));
}
}

return $allConfigFiles;
}

private static function expandIncludedFile(string $includedFile, string $mainFile): string
{
return Strings::match($includedFile, '#([a-z]+:)?[/\\\\]#Ai') !== null // is absolute
? $includedFile
: dirname($mainFile) . '/' . $includedFile;
}

}
28 changes: 28 additions & 0 deletions src/DependencyInjection/DuplicateIncludedFilesException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection;

use Exception;
use function implode;
use function sprintf;

class DuplicateIncludedFilesException extends Exception
{

/**
* @param string[] $files
*/
public function __construct(private array $files)
{
parent::__construct(sprintf('These files are included multiple times: %s', implode(', ', $this->files)));
}

/**
* @return string[]
*/
public function getFiles(): array
{
return $this->files;
}

}

0 comments on commit f15cd6d

Please sign in to comment.