From 0c17a5bac7ef0576b9899147407294ce5832123f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 13 Sep 2019 11:32:21 +0200 Subject: [PATCH] [BCB] Resolving relative paths in NEON according to config file placement --- README.md | 6 +- build/phpstan.neon | 3 + composer.json | 1 + src/Command/CommandHelper.php | 18 +- src/DependencyInjection/Configurator.php | 14 ++ src/DependencyInjection/LoaderFactory.php | 2 +- src/DependencyInjection/NeonAdapter.php | 170 ++++++++++++++++++ tests/PHPStan/Command/CommandHelperTest.php | 85 ++++++++- tests/PHPStan/Command/relative-paths/here.php | 1 + .../Command/relative-paths/nested/here.php | 1 + .../Command/relative-paths/nested/nested.neon | 14 ++ .../relative-paths/nested/test/there.php | 1 + .../PHPStan/Command/relative-paths/root.neon | 15 ++ .../Command/relative-paths/src/.gitkeep | 0 .../Command/relative-paths/test/there.php | 1 + tests/PHPStan/Command/relative-paths/up.php | 1 + tests/PHPStan/Command/up.php | 1 + 17 files changed, 323 insertions(+), 11 deletions(-) create mode 100644 src/DependencyInjection/NeonAdapter.php create mode 100644 tests/PHPStan/Command/relative-paths/here.php create mode 100644 tests/PHPStan/Command/relative-paths/nested/here.php create mode 100644 tests/PHPStan/Command/relative-paths/nested/nested.neon create mode 100644 tests/PHPStan/Command/relative-paths/nested/test/there.php create mode 100644 tests/PHPStan/Command/relative-paths/root.neon create mode 100644 tests/PHPStan/Command/relative-paths/src/.gitkeep create mode 100644 tests/PHPStan/Command/relative-paths/test/there.php create mode 100644 tests/PHPStan/Command/relative-paths/up.php create mode 100644 tests/PHPStan/Command/up.php diff --git a/README.md b/README.md index 2faa2ec1f5..fa1cfa034c 100644 --- a/README.md +++ b/README.md @@ -191,9 +191,9 @@ you can specify directories to scan and concrete files to include using ``` parameters: autoload_directories: - - %rootDir%/../../../build + - build autoload_files: - - %rootDir%/../../../generated/routes/GeneratedRouteList.php + - generated/routes/GeneratedRouteList.php ``` `%rootDir%` is expanded to the root directory where PHPStan resides. @@ -364,7 +364,7 @@ you can provide your own bootstrap file: ``` parameters: - bootstrap: %rootDir%/../../../phpstan-bootstrap.php + bootstrap: phpstan-bootstrap.php ``` ### Custom rules diff --git a/build/phpstan.neon b/build/phpstan.neon index 32e1ddd77f..7acc06dd18 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -50,6 +50,9 @@ parameters: - message: '#^Constant SOME_CONSTANT_IN_AUTOLOAD_FILE not found\.$#' path: %rootDir%/tests/PHPStan/Command/AnalyseCommandTest.php + - + message: '#^Fetching class constant PREVENT_MERGING of deprecated class Nette\\DI\\Config\\Helpers\.$#' + path: ../src/DependencyInjection/NeonAdapter.php reportStaticMethodSignatures: true reportUnmatchedIgnoredErrors: false tmpDir: %rootDir%/tmp diff --git a/composer.json b/composer.json index 0133f0c496..925d7f6c4a 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "jean85/pretty-package-versions": "^1.0.3", "nette/bootstrap": "^3.0", "nette/di": "^3.0", + "nette/neon": "^3.0", "nette/robot-loader": "^3.0.1", "nette/schema": "^1.0", "nette/utils": "^3.0", diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 6dc0628e47..e80c889d6b 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -2,7 +2,6 @@ namespace PHPStan\Command; -use Nette\DI\Config\Adapters\NeonAdapter; use Nette\DI\Config\Adapters\PhpAdapter; use Nette\DI\Helpers; use Nette\Schema\Context as SchemaContext; @@ -11,6 +10,7 @@ use Nette\Utils\Validators; use PHPStan\DependencyInjection\ContainerFactory; use PHPStan\DependencyInjection\LoaderFactory; +use PHPStan\DependencyInjection\NeonAdapter; use PHPStan\File\FileFinder; use PHPStan\File\FileHelper; use Symfony\Component\Console\Input\InputInterface; @@ -53,7 +53,6 @@ public static function begin( } $fileHelper = new FileHelper($currentWorkingDirectory); if ($autoloadFile !== null) { - $autoloadFile = $fileHelper->absolutizePath($autoloadFile); if (!is_file($autoloadFile)) { $errorOutput->writeln(sprintf('Autoload file "%s" not found.', $autoloadFile)); throw new \PHPStan\Command\InceptionNotSuccessfulException(); @@ -243,9 +242,13 @@ public static function begin( } foreach ($container->getParameter('autoload_files') as $parameterAutoloadFile) { + if (!file_exists($parameterAutoloadFile)) { + $errorOutput->writeln(sprintf('Autoload file %s does not exist.', $parameterAutoloadFile)); + throw new \PHPStan\Command\InceptionNotSuccessfulException(); + } (static function (string $file) use ($container): void { require_once $file; - })($fileHelper->normalizePath($parameterAutoloadFile)); + })($parameterAutoloadFile); } $autoloadDirectories = $container->getParameter('autoload_directories'); @@ -257,11 +260,15 @@ public static function begin( $robotLoader->setTempDirectory($tmpDir); foreach ($autoloadDirectories as $directory) { - $robotLoader->addDirectory($fileHelper->normalizePath($directory)); + if (!file_exists($directory)) { + $errorOutput->writeln(sprintf('Autoload directory %s does not exist.', $directory)); + throw new \PHPStan\Command\InceptionNotSuccessfulException(); + } + $robotLoader->addDirectory($directory); } foreach ($container->getParameter('excludes_analyse') as $directory) { - $robotLoader->excludeDirectory($fileHelper->normalizePath($directory)); + $robotLoader->excludeDirectory($directory); } $robotLoader->register(); @@ -269,7 +276,6 @@ public static function begin( $bootstrapFile = $container->getParameter('bootstrap'); if ($bootstrapFile !== null) { - $bootstrapFile = $fileHelper->normalizePath($bootstrapFile); if (!is_file($bootstrapFile)) { $errorOutput->writeln(sprintf('Bootstrap file %s does not exist.', $bootstrapFile)); throw new \PHPStan\Command\InceptionNotSuccessfulException(); diff --git a/src/DependencyInjection/Configurator.php b/src/DependencyInjection/Configurator.php index 942f38e9cb..57c8476329 100644 --- a/src/DependencyInjection/Configurator.php +++ b/src/DependencyInjection/Configurator.php @@ -3,6 +3,7 @@ namespace PHPStan\DependencyInjection; use Nette\DI\Config\Loader; +use Nette\DI\ContainerLoader; class Configurator extends \Nette\Configurator { @@ -30,4 +31,17 @@ protected function getDefaultParameters(): array return []; } + public function loadContainer(): string + { + $loader = new ContainerLoader( + $this->getCacheDirectory() . '/nette.configurator', + $this->parameters['debugMode'] + ); + + return $loader->load( + [$this, 'generateContainer'], + [$this->parameters, array_keys($this->dynamicParameters), $this->configs, PHP_VERSION_ID - PHP_RELEASE_VERSION, NeonAdapter::CACHE_KEY] + ); + } + } diff --git a/src/DependencyInjection/LoaderFactory.php b/src/DependencyInjection/LoaderFactory.php index 190de4f654..97eb885427 100644 --- a/src/DependencyInjection/LoaderFactory.php +++ b/src/DependencyInjection/LoaderFactory.php @@ -2,7 +2,6 @@ namespace PHPStan\DependencyInjection; -use Nette\DI\Config\Adapters\NeonAdapter; use Nette\DI\Config\Loader; class LoaderFactory @@ -27,6 +26,7 @@ public function createLoader(): Loader { $loader = new Loader(); $loader->addAdapter('dist', NeonAdapter::class); + $loader->addAdapter('neon', NeonAdapter::class); $loader->setParameters([ 'rootDir' => $this->rootDir, 'currentWorkingDirectory' => $this->currentWorkingDirectory, diff --git a/src/DependencyInjection/NeonAdapter.php b/src/DependencyInjection/NeonAdapter.php new file mode 100644 index 0000000000..cbfbc4012f --- /dev/null +++ b/src/DependencyInjection/NeonAdapter.php @@ -0,0 +1,170 @@ +process((array) Neon::decode($contents), '', $file); + } + + /** + * @param mixed[] $arr + * @return mixed[] + */ + public function process(array $arr, string $fileKey, string $file): array + { + $res = []; + foreach ($arr as $key => $val) { + if (is_string($key) && substr($key, -1) === self::PREVENT_MERGING_SUFFIX) { + if (!is_array($val) && $val !== null) { + throw new \Nette\DI\InvalidConfigurationException(sprintf('Replacing operator is available only for arrays, item \'%s\' is not array.', $key)); + } + $key = substr($key, 0, -1); + $val[Helpers::PREVENT_MERGING] = true; + } + + if (is_array($val)) { + if (!is_int($key)) { + $fileKeyToPass = $fileKey . '[' . $key . ']'; + } else { + $fileKeyToPass = $fileKey . '[]'; + } + $val = $this->process($val, $fileKeyToPass, $file); + + } elseif ($val instanceof Entity) { + if (!is_int($key)) { + $fileKeyToPass = $fileKey . '(' . $key . ')'; + } else { + $fileKeyToPass = $fileKey . '()'; + } + if ($val->value === Neon::CHAIN) { + $tmp = null; + foreach ($this->process($val->attributes, $fileKeyToPass, $file) as $st) { + $tmp = new Statement( + $tmp === null ? $st->getEntity() : [$tmp, ltrim(implode('::', (array) $st->getEntity()), ':')], + $st->arguments + ); + } + $val = $tmp; + } else { + $tmp = $this->process([$val->value], $fileKeyToPass, $file); + $val = new Statement($tmp[0], $this->process($val->attributes, $fileKeyToPass, $file)); + } + } + if ((in_array($fileKey, [ + '[parameters][autoload_files]', + '[parameters][autoload_directories]', + '[parameters][paths]', + '[parameters][excludes_analyse]', + '[parameters][ignoreErrors][][paths]', + ], true) || ( + $fileKey === '[parameters]' + && in_array($key, [ + 'bootstrap', + 'memoryLimitFile', + 'benchmarkFile', + ], true) + ) || ( + $fileKey === '[parameters][ignoreErrors][]' + && in_array($key, [ + 'path', + ], true) + )) && is_string($val) && strpos($val, '%') === false) { + $fileHelper = $this->createFileHelperByFile($file); + $val = $fileHelper->normalizePath($fileHelper->absolutizePath($val)); + } + $res[$key] = $val; + } + return $res; + } + + /** + * @param mixed[] $data + * @return string + */ + public function dump(array $data): string + { + array_walk_recursive( + $data, + static function (&$val): void { + if (!($val instanceof Statement)) { + return; + } + + $val = self::statementToEntity($val); + } + ); + return "# generated by Nette\n\n" . Neon::encode($data, Neon::BLOCK); + } + + private static function statementToEntity(Statement $val): Entity + { + array_walk_recursive( + $val->arguments, + static function (&$val): void { + if ($val instanceof Statement) { + $val = self::statementToEntity($val); + } elseif ($val instanceof Reference) { + $val = '@' . $val->getValue(); + } + } + ); + + $entity = $val->getEntity(); + if ($entity instanceof Reference) { + $entity = '@' . $entity->getValue(); + } elseif (is_array($entity)) { + if ($entity[0] instanceof Statement) { + return new Entity( + Neon::CHAIN, + [ + self::statementToEntity($entity[0]), + new Entity('::' . $entity[1], $val->arguments), + ] + ); + } elseif ($entity[0] instanceof Reference) { + $entity = '@' . $entity[0]->getValue() . '::' . $entity[1]; + } elseif (is_string($entity[0])) { + $entity = $entity[0] . '::' . $entity[1]; + } + } + return new Entity($entity, $val->arguments); + } + + private function createFileHelperByFile(string $file): FileHelper + { + $dir = dirname($file); + if (!isset($this->fileHelpers[$dir])) { + $this->fileHelpers[$dir] = new FileHelper($dir); + } + + return $this->fileHelpers[$dir]; + } + +} diff --git a/tests/PHPStan/Command/CommandHelperTest.php b/tests/PHPStan/Command/CommandHelperTest.php index 93ac0a2a7c..b5ce88a6aa 100644 --- a/tests/PHPStan/Command/CommandHelperTest.php +++ b/tests/PHPStan/Command/CommandHelperTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\StreamOutput; class CommandHelperTest extends TestCase @@ -123,7 +124,12 @@ public function testBegin( } } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { if (!$expectException) { - throw $e; + rewind($output->getStream()); + $contents = stream_get_contents($output->getStream()); + if ($contents === false) { + throw new \PHPStan\ShouldNotHappenException(); + } + $this->fail($contents); } } @@ -146,4 +152,81 @@ public function testBegin( } } + public function dataResolveRelativePaths(): array + { + return [ + [ + __DIR__ . '/relative-paths/root.neon', + [ + 'bootstrap' => __DIR__ . '/relative-paths/here.php', + 'autoload_files' => [ + __DIR__ . '/relative-paths/here.php', + __DIR__ . '/relative-paths/test/there.php', + __DIR__ . '/up.php', + ], + 'autoload_directories' => [ + __DIR__ . '/relative-paths/src', + __DIR__ . '/relative-paths', + realpath(__DIR__ . '/../../../conf'), + ], + 'paths' => [ + __DIR__ . '/relative-paths/src', + ], + 'memoryLimitFile' => __DIR__ . '/relative-paths/.memory_limit', + 'excludes_analyse' => [__DIR__ . '/relative-paths/src'], + ], + ], + [ + __DIR__ . '/relative-paths/nested/nested.neon', + [ + 'autoload_files' => [ + __DIR__ . '/relative-paths/nested/here.php', + __DIR__ . '/relative-paths/nested/test/there.php', + __DIR__ . '/relative-paths/up.php', + ], + 'ignoreErrors' => [ + [ + 'message' => '#aaa#', + 'path' => __DIR__ . '/relative-paths/nested/src/aaa.php', + ], + [ + 'message' => '#bbb#', + 'paths' => [ + __DIR__ . '/relative-paths/src/aaa.php', + __DIR__ . '/relative-paths/nested/src/bbb.php', + ], + ], + ], + ], + ], + ]; + } + + /** + * @dataProvider dataResolveRelativePaths + * @param string $configFile + * @param array $expectedParameters + */ + public function testResolveRelativePaths( + string $configFile, + array $expectedParameters + ): void + { + $result = CommandHelper::begin( + new StringInput(''), + new NullOutput(), + [__DIR__], + null, + null, + null, + $configFile, + '0' + ); + $parameters = $result->getContainer()->getParameters(); + foreach ($expectedParameters as $name => $expectedValue) { + $this->assertArrayHasKey($name, $parameters); + $this->assertSame($expectedValue, $parameters[$name]); + } + } + } diff --git a/tests/PHPStan/Command/relative-paths/here.php b/tests/PHPStan/Command/relative-paths/here.php new file mode 100644 index 0000000000..3c6b265174 --- /dev/null +++ b/tests/PHPStan/Command/relative-paths/here.php @@ -0,0 +1 @@ +