Skip to content

Commit f15cd6d

Browse files
committed
Detect duplicate included files in ContainerFactory
1 parent 91496e1 commit f15cd6d

File tree

4 files changed

+138
-105
lines changed

4 files changed

+138
-105
lines changed

build/phpstan.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ parameters:
4545
- 'Symfony\Component\Finder\Exception\DirectoryNotFoundException'
4646
- 'InvalidArgumentException'
4747
- 'PHPStan\DependencyInjection\ParameterNotFoundException'
48+
- 'PHPStan\DependencyInjection\DuplicateIncludedFilesException'
4849
- 'PHPStan\Analyser\UndefinedVariableException'
4950
- 'RuntimeException'
5051
- 'Nette\Neon\Exception'

src/Command/CommandHelper.php

Lines changed: 14 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use Closure;
66
use Composer\XdebugHandler\XdebugHandler;
7-
use Nette\DI\Config\Adapters\PhpAdapter;
87
use Nette\DI\Helpers;
98
use Nette\DI\InvalidConfigurationException;
109
use Nette\DI\ServiceCreationException;
@@ -13,14 +12,13 @@
1312
use Nette\Schema\ValidationException;
1413
use Nette\Utils\AssertionException;
1514
use Nette\Utils\Strings;
16-
use Nette\Utils\Validators;
1715
use PHPStan\Command\Symfony\SymfonyOutput;
1816
use PHPStan\Command\Symfony\SymfonyStyle;
1917
use PHPStan\DependencyInjection\Container;
2018
use PHPStan\DependencyInjection\ContainerFactory;
19+
use PHPStan\DependencyInjection\DuplicateIncludedFilesException;
2120
use PHPStan\DependencyInjection\InvalidIgnoredErrorPatternsException;
2221
use PHPStan\DependencyInjection\LoaderFactory;
23-
use PHPStan\DependencyInjection\NeonAdapter;
2422
use PHPStan\ExtensionInstaller\GeneratedConfig;
2523
use PHPStan\File\FileFinder;
2624
use PHPStan\File\FileHelper;
@@ -30,11 +28,8 @@
3028
use Symfony\Component\Console\Output\ConsoleOutputInterface;
3129
use Symfony\Component\Console\Output\OutputInterface;
3230
use Throwable;
33-
use function array_diff_key;
3431
use function array_key_exists;
3532
use function array_map;
36-
use function array_merge;
37-
use function array_unique;
3833
use function class_exists;
3934
use function count;
4035
use function dirname;
@@ -53,7 +48,6 @@
5348
use function register_shutdown_function;
5449
use function spl_autoload_functions;
5550
use function sprintf;
56-
use function str_ends_with;
5751
use function str_repeat;
5852
use function strpos;
5953
use function sys_get_temp_dir;
@@ -270,18 +264,6 @@ public static function begin(
270264
$additionalConfigFiles[] = $projectConfigFile;
271265
}
272266

273-
$loaderParameters = [
274-
'rootDir' => $containerFactory->getRootDirectory(),
275-
'currentWorkingDirectory' => $containerFactory->getCurrentWorkingDirectory(),
276-
];
277-
278-
self::detectDuplicateIncludedFiles(
279-
$errorOutput,
280-
$currentWorkingDirectoryFileHelper,
281-
$additionalConfigFiles,
282-
$loaderParameters,
283-
);
284-
285267
$createDir = static function (string $path) use ($errorOutput): void {
286268
if (!is_dir($path) && !@mkdir($path, 0777) && !is_dir($path)) {
287269
$errorOutput->writeLineFormatted(sprintf('Cannot create a temp directory %s', $path));
@@ -343,6 +325,19 @@ public static function begin(
343325
$errorOutput->writeLineFormatted(sprintf("\t\t%s: @defaultAnalysisParser", $matches['parameterName']));
344326
$errorOutput->writeLineFormatted('');
345327

328+
throw new InceptionNotSuccessfulException();
329+
} catch (DuplicateIncludedFilesException $e) {
330+
$format = "<error>These files are included multiple times:</error>\n- %s";
331+
if (count($e->getFiles()) === 1) {
332+
$format = "<error>This file is included multiple times:</error>\n- %s";
333+
}
334+
$errorOutput->writeLineFormatted(sprintf($format, implode("\n- ", $e->getFiles())));
335+
336+
if (class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) {
337+
$errorOutput->writeLineFormatted('');
338+
$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.');
339+
}
340+
346341
throw new InceptionNotSuccessfulException();
347342
}
348343

@@ -516,90 +511,4 @@ private static function executeBootstrapFile(
516511
}
517512
}
518513

519-
/**
520-
* @param string[] $configFiles
521-
* @param array<string, string> $loaderParameters
522-
* @throws InceptionNotSuccessfulException
523-
*/
524-
private static function detectDuplicateIncludedFiles(
525-
Output $output,
526-
FileHelper $fileHelper,
527-
array $configFiles,
528-
array $loaderParameters,
529-
): void
530-
{
531-
$neonAdapter = new NeonAdapter();
532-
$phpAdapter = new PhpAdapter();
533-
$allConfigFiles = [];
534-
foreach ($configFiles as $configFile) {
535-
$allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null));
536-
}
537-
538-
$normalized = array_map(static fn (string $file): string => $fileHelper->normalizePath($file), $allConfigFiles);
539-
540-
$deduplicated = array_unique($normalized);
541-
if (count($normalized) <= count($deduplicated)) {
542-
return;
543-
}
544-
545-
$duplicateFiles = array_unique(array_diff_key($normalized, $deduplicated));
546-
547-
$format = "<error>These files are included multiple times:</error>\n- %s";
548-
if (count($duplicateFiles) === 1) {
549-
$format = "<error>This file is included multiple times:</error>\n- %s";
550-
}
551-
$output->writeLineFormatted(sprintf($format, implode("\n- ", $duplicateFiles)));
552-
553-
if (class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) {
554-
$output->writeLineFormatted('');
555-
$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.');
556-
}
557-
throw new InceptionNotSuccessfulException();
558-
}
559-
560-
/**
561-
* @param array<string, string> $loaderParameters
562-
* @return string[]
563-
*/
564-
private static function getConfigFiles(
565-
FileHelper $fileHelper,
566-
NeonAdapter $neonAdapter,
567-
PhpAdapter $phpAdapter,
568-
string $configFile,
569-
array $loaderParameters,
570-
?string $generateBaselineFile,
571-
): array
572-
{
573-
if ($generateBaselineFile === $fileHelper->normalizePath($configFile)) {
574-
return [];
575-
}
576-
if (!is_file($configFile) || !is_readable($configFile)) {
577-
return [];
578-
}
579-
580-
if (str_ends_with($configFile, '.php')) {
581-
$data = $phpAdapter->load($configFile);
582-
} else {
583-
$data = $neonAdapter->load($configFile);
584-
}
585-
$allConfigFiles = [$configFile];
586-
if (isset($data['includes'])) {
587-
Validators::assert($data['includes'], 'list', sprintf("section 'includes' in file '%s'", $configFile));
588-
$includes = Helpers::expand($data['includes'], $loaderParameters);
589-
foreach ($includes as $include) {
590-
$include = self::expandIncludedFile($include, $configFile);
591-
$allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $include, $loaderParameters, $generateBaselineFile));
592-
}
593-
}
594-
595-
return $allConfigFiles;
596-
}
597-
598-
private static function expandIncludedFile(string $includedFile, string $mainFile): string
599-
{
600-
return Strings::match($includedFile, '#([a-z]+:)?[/\\\\]#Ai') !== null // is absolute
601-
? $includedFile
602-
: dirname($mainFile) . '/' . $includedFile;
603-
}
604-
605514
}

src/DependencyInjection/ContainerFactory.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
namespace PHPStan\DependencyInjection;
44

5+
use Nette\DI\Config\Adapters\PhpAdapter;
56
use Nette\DI\Extensions\ExtensionsExtension;
67
use Nette\DI\Extensions\PhpExtension;
8+
use Nette\DI\Helpers;
9+
use Nette\Utils\Strings;
10+
use Nette\Utils\Validators;
711
use Phar;
812
use PhpParser\Parser;
913
use PHPStan\BetterReflection\BetterReflection;
@@ -17,10 +21,19 @@
1721
use PHPStan\Reflection\ReflectionProvider;
1822
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
1923
use Symfony\Component\Finder\Finder;
24+
use function array_diff_key;
25+
use function array_map;
26+
use function array_merge;
27+
use function array_unique;
28+
use function count;
2029
use function dirname;
2130
use function extension_loaded;
2231
use function ini_get;
2332
use function is_dir;
33+
use function is_file;
34+
use function is_readable;
35+
use function sprintf;
36+
use function str_ends_with;
2437
use function sys_get_temp_dir;
2538
use function time;
2639
use function unlink;
@@ -71,6 +84,14 @@ public function create(
7184
?string $singleReflectionInsteadOfFile = null,
7285
): Container
7386
{
87+
$this->detectDuplicateIncludedFiles(
88+
$additionalConfigFiles,
89+
[
90+
'rootDir' => $this->rootDirectory,
91+
'currentWorkingDirectory' => $this->currentWorkingDirectory,
92+
],
93+
);
94+
7495
$configurator = new Configurator(new LoaderFactory(
7596
$this->fileHelper,
7697
$this->rootDirectory,
@@ -187,4 +208,78 @@ public function getConfigDirectory(): string
187208
return $this->configDirectory;
188209
}
189210

211+
/**
212+
* @param string[] $configFiles
213+
* @param array<string, string> $loaderParameters
214+
* @throws DuplicateIncludedFilesException
215+
*/
216+
private function detectDuplicateIncludedFiles(
217+
array $configFiles,
218+
array $loaderParameters,
219+
): void
220+
{
221+
$neonAdapter = new NeonAdapter();
222+
$phpAdapter = new PhpAdapter();
223+
$allConfigFiles = [];
224+
foreach ($configFiles as $configFile) {
225+
$allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($this->fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null));
226+
}
227+
228+
$normalized = array_map(fn (string $file): string => $this->fileHelper->normalizePath($file), $allConfigFiles);
229+
230+
$deduplicated = array_unique($normalized);
231+
if (count($normalized) <= count($deduplicated)) {
232+
return;
233+
}
234+
235+
$duplicateFiles = array_unique(array_diff_key($normalized, $deduplicated));
236+
237+
throw new DuplicateIncludedFilesException($duplicateFiles);
238+
}
239+
240+
/**
241+
* @param array<string, string> $loaderParameters
242+
* @return string[]
243+
*/
244+
private static function getConfigFiles(
245+
FileHelper $fileHelper,
246+
NeonAdapter $neonAdapter,
247+
PhpAdapter $phpAdapter,
248+
string $configFile,
249+
array $loaderParameters,
250+
?string $generateBaselineFile,
251+
): array
252+
{
253+
if ($generateBaselineFile === $fileHelper->normalizePath($configFile)) {
254+
return [];
255+
}
256+
if (!is_file($configFile) || !is_readable($configFile)) {
257+
return [];
258+
}
259+
260+
if (str_ends_with($configFile, '.php')) {
261+
$data = $phpAdapter->load($configFile);
262+
} else {
263+
$data = $neonAdapter->load($configFile);
264+
}
265+
$allConfigFiles = [$configFile];
266+
if (isset($data['includes'])) {
267+
Validators::assert($data['includes'], 'list', sprintf("section 'includes' in file '%s'", $configFile));
268+
$includes = Helpers::expand($data['includes'], $loaderParameters);
269+
foreach ($includes as $include) {
270+
$include = self::expandIncludedFile($include, $configFile);
271+
$allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $include, $loaderParameters, $generateBaselineFile));
272+
}
273+
}
274+
275+
return $allConfigFiles;
276+
}
277+
278+
private static function expandIncludedFile(string $includedFile, string $mainFile): string
279+
{
280+
return Strings::match($includedFile, '#([a-z]+:)?[/\\\\]#Ai') !== null // is absolute
281+
? $includedFile
282+
: dirname($mainFile) . '/' . $includedFile;
283+
}
284+
190285
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\DependencyInjection;
4+
5+
use Exception;
6+
use function implode;
7+
use function sprintf;
8+
9+
class DuplicateIncludedFilesException extends Exception
10+
{
11+
12+
/**
13+
* @param string[] $files
14+
*/
15+
public function __construct(private array $files)
16+
{
17+
parent::__construct(sprintf('These files are included multiple times: %s', implode(', ', $this->files)));
18+
}
19+
20+
/**
21+
* @return string[]
22+
*/
23+
public function getFiles(): array
24+
{
25+
return $this->files;
26+
}
27+
28+
}

0 commit comments

Comments
 (0)