From 445b8d929ae409dd7d1b21a5a6b8e21c3d041422 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 22 Aug 2020 14:24:09 +0200 Subject: [PATCH 1/3] Parametrize IgnoreWhitespaceTest --- tests/IgnoreWhitespaceTest.php | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/tests/IgnoreWhitespaceTest.php b/tests/IgnoreWhitespaceTest.php index aea50059..e4430242 100644 --- a/tests/IgnoreWhitespaceTest.php +++ b/tests/IgnoreWhitespaceTest.php @@ -15,9 +15,15 @@ */ final class IgnoreWhitespaceTest extends TestCase { - public function testIgnoreWhitespaces(): void + + /** + * @return string[][] + */ + public function provideIgnoreWhitespaces(): array { - $old = <<<'PHP' + return [ + [ + <<<'OLD' true, ], [ 'cliColorization' => RendererConstant::CLI_COLOR_DISABLE, ]); - static::assertSame($expected, $diff); + static::assertSame($expectedDiff, $diff); } } From a4bd84d43207844318df87206f981bfb6657a80f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 22 Aug 2020 14:37:33 +0200 Subject: [PATCH 2/3] Passing test --- tests/IgnoreWhitespaceTest.php | 50 ++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/IgnoreWhitespaceTest.php b/tests/IgnoreWhitespaceTest.php index e4430242..767d0115 100644 --- a/tests/IgnoreWhitespaceTest.php +++ b/tests/IgnoreWhitespaceTest.php @@ -57,6 +57,56 @@ function foo(\DateTimeImmutable $date) - } } +DIFF + ], + [ + <<<'OLD' + Date: Sat, 22 Aug 2020 14:57:12 +0200 Subject: [PATCH 3/3] Failing test --- tests/IgnoreWhitespaceTest.php | 19 +++ tests/data/WorkerCommandA.php | 225 +++++++++++++++++++++++++++++++++ tests/data/WorkerCommandB.php | 220 ++++++++++++++++++++++++++++++++ 3 files changed, 464 insertions(+) create mode 100644 tests/data/WorkerCommandA.php create mode 100644 tests/data/WorkerCommandB.php diff --git a/tests/IgnoreWhitespaceTest.php b/tests/IgnoreWhitespaceTest.php index 767d0115..7ec28b4c 100644 --- a/tests/IgnoreWhitespaceTest.php +++ b/tests/IgnoreWhitespaceTest.php @@ -107,6 +107,25 @@ function foo() } +DIFF + ], + [ + file_get_contents(__DIR__ . '/data/WorkerCommandA.php'), + file_get_contents(__DIR__ . '/data/WorkerCommandB.php'), + <<<'DIFF' +@@ -215,11 +215,6 @@ + { + echo 'haha'; + return; +- +- echo 'blabla'; +- if (false) { +- +- } + } + + } + DIFF ], ]; diff --git a/tests/data/WorkerCommandA.php b/tests/data/WorkerCommandA.php new file mode 100644 index 00000000..94709f5e --- /dev/null +++ b/tests/data/WorkerCommandA.php @@ -0,0 +1,225 @@ +composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; + } + + protected function configure(): void + { + $this->setName(self::NAME) + ->setDescription('(Internal) Support for parallel analysis.') + ->setDefinition([ + new InputArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Paths with source code to run analysis on'), + new InputOption('paths-file', null, InputOption::VALUE_REQUIRED, 'Path to a file with a list of paths to run analysis on'), + new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), + new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), + new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), + new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'), + new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), + new InputOption('port', null, InputOption::VALUE_REQUIRED), + new InputOption('identifier', null, InputOption::VALUE_REQUIRED), + ]); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $paths = $input->getArgument('paths'); + $memoryLimit = $input->getOption('memory-limit'); + $autoloadFile = $input->getOption('autoload-file'); + $configuration = $input->getOption('configuration'); + $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); + $pathsFile = $input->getOption('paths-file'); + $allowXdebug = $input->getOption('xdebug'); + $port = $input->getOption('port'); + $identifier = $input->getOption('identifier'); + + if ( + !is_array($paths) + || (!is_string($memoryLimit) && $memoryLimit !== null) + || (!is_string($autoloadFile) && $autoloadFile !== null) + || (!is_string($configuration) && $configuration !== null) + || (!is_string($level) && $level !== null) + || (!is_string($pathsFile) && $pathsFile !== null) + || (!is_bool($allowXdebug)) + || !is_string($port) + || !is_string($identifier) + ) { + throw new \PHPStan\ShouldNotHappenException(); + } + + try { + $inceptionResult = CommandHelper::begin( + $input, + $output, + $paths, + $pathsFile, + $memoryLimit, + $autoloadFile, + $this->composerAutoloaderProjectPaths, + $configuration, + null, + $level, + $allowXdebug, + false + ); + } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + return 1; + } + $loop = new StreamSelectLoop(); + + $container = $inceptionResult->getContainer(); + + $analysedFiles = $inceptionResult->getFiles(); + + /** @var NodeScopeResolver $nodeScopeResolver */ + $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); + $nodeScopeResolver->setAnalysedFiles($analysedFiles); + + $analysedFiles = array_fill_keys($analysedFiles, true); + + $tcpConector = new TcpConnector($loop); + $tcpConector->connect(sprintf('127.0.0.1:%d', $port))->done(function (ConnectionInterface $connection) use ($container, $identifier, $analysedFiles): void { + $out = new Encoder($connection); + $in = new Decoder($connection, true, 512, 0, $container->getParameter('parallel')['buffer']); + $out->write(['action' => 'hello', 'identifier' => $identifier]); + $this->runWorker($container, $out, $in, $analysedFiles); + }); + + $loop->run(); + + return 0; + } + + /** + * @param Container $container + * @param WritableStreamInterface $out + * @param ReadableStreamInterface $in + * @param array $analysedFiles + */ + private function runWorker( + Container $container, + WritableStreamInterface $out, + ReadableStreamInterface $in, + array $analysedFiles + ): void + { + $handleError = static function (\Throwable $error) use ($out): void { + $out->write([ + 'action' => 'result', + 'result' => [ + 'errors' => [$error->getMessage()], + 'dependencies' => [], + 'filesCount' => 0, + 'internalErrorsCount' => 1, + ], + ]); + $out->end(); + }; + $out->on('error', $handleError); + + /** @var FileAnalyser $fileAnalyser */ + $fileAnalyser = $container->getByType(FileAnalyser::class); + + /** @var Registry $registry */ + $registry = $container->getByType(Registry::class); + + // todo collectErrors (from Analyser) + $in->on('data', static function (array $json) use ($fileAnalyser, $registry, $out, $analysedFiles): void { + $action = $json['action']; + if ($action !== 'analyse') { + return; + } + + $internalErrorsCount = 0; + $files = $json['files']; + $errors = []; + $dependencies = []; + foreach ($files as $file) { + try { + $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $registry, null); + $fileErrors = $fileAnalyserResult->getErrors(); + $dependencies[$file] = $fileAnalyserResult->getDependencies(); + foreach ($fileErrors as $fileError) { + $errors[] = $fileError; + } + } catch (\Throwable $t) { + $internalErrorsCount++; + $internalErrorMessage = sprintf('Internal error: %s in file %s', $t->getMessage(), $file); + $internalErrorMessage .= sprintf( + '%sRun PHPStan with --debug option and post the stack trace to:%s%s', + "\n", + "\n", + 'https://github.com/phpstan/phpstan/issues/new' + ); + $errors[] = $internalErrorMessage; + } + } + + $out->write([ + 'action' => 'result', + 'result' => [ + 'errors' => $errors, + 'dependencies' => $dependencies, + 'filesCount' => count($files), + 'internalErrorsCount' => $internalErrorsCount, + ]]); + }); + $in->on('error', $handleError); + } + + public function haha(int $i): void + { + function () use ($i) { + + }; + } + + /** + * @param int $b + */ + public function doLorem(int $a): void + { + echo 'haha'; + return; + + echo 'blabla'; + if (false) { + + } + } + +} diff --git a/tests/data/WorkerCommandB.php b/tests/data/WorkerCommandB.php new file mode 100644 index 00000000..8453dfed --- /dev/null +++ b/tests/data/WorkerCommandB.php @@ -0,0 +1,220 @@ +composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; + } + + protected function configure(): void + { + $this->setName(self::NAME) + ->setDescription('(Internal) Support for parallel analysis.') + ->setDefinition([ + new InputArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Paths with source code to run analysis on'), + new InputOption('paths-file', null, InputOption::VALUE_REQUIRED, 'Path to a file with a list of paths to run analysis on'), + new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), + new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), + new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), + new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'), + new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), + new InputOption('port', null, InputOption::VALUE_REQUIRED), + new InputOption('identifier', null, InputOption::VALUE_REQUIRED), + ]); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $paths = $input->getArgument('paths'); + $memoryLimit = $input->getOption('memory-limit'); + $autoloadFile = $input->getOption('autoload-file'); + $configuration = $input->getOption('configuration'); + $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); + $pathsFile = $input->getOption('paths-file'); + $allowXdebug = $input->getOption('xdebug'); + $port = $input->getOption('port'); + $identifier = $input->getOption('identifier'); + + if ( + !is_array($paths) + || (!is_string($memoryLimit) && $memoryLimit !== null) + || (!is_string($autoloadFile) && $autoloadFile !== null) + || (!is_string($configuration) && $configuration !== null) + || (!is_string($level) && $level !== null) + || (!is_string($pathsFile) && $pathsFile !== null) + || (!is_bool($allowXdebug)) + || !is_string($port) + || !is_string($identifier) + ) { + throw new \PHPStan\ShouldNotHappenException(); + } + + try { + $inceptionResult = CommandHelper::begin( + $input, + $output, + $paths, + $pathsFile, + $memoryLimit, + $autoloadFile, + $this->composerAutoloaderProjectPaths, + $configuration, + null, + $level, + $allowXdebug, + false + ); + } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + return 1; + } + $loop = new StreamSelectLoop(); + + $container = $inceptionResult->getContainer(); + + $analysedFiles = $inceptionResult->getFiles(); + + /** @var NodeScopeResolver $nodeScopeResolver */ + $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); + $nodeScopeResolver->setAnalysedFiles($analysedFiles); + + $analysedFiles = array_fill_keys($analysedFiles, true); + + $tcpConector = new TcpConnector($loop); + $tcpConector->connect(sprintf('127.0.0.1:%d', $port))->done(function (ConnectionInterface $connection) use ($container, $identifier, $analysedFiles): void { + $out = new Encoder($connection); + $in = new Decoder($connection, true, 512, 0, $container->getParameter('parallel')['buffer']); + $out->write(['action' => 'hello', 'identifier' => $identifier]); + $this->runWorker($container, $out, $in, $analysedFiles); + }); + + $loop->run(); + + return 0; + } + + /** + * @param Container $container + * @param WritableStreamInterface $out + * @param ReadableStreamInterface $in + * @param array $analysedFiles + */ + private function runWorker( + Container $container, + WritableStreamInterface $out, + ReadableStreamInterface $in, + array $analysedFiles + ): void + { + $handleError = static function (\Throwable $error) use ($out): void { + $out->write([ + 'action' => 'result', + 'result' => [ + 'errors' => [$error->getMessage()], + 'dependencies' => [], + 'filesCount' => 0, + 'internalErrorsCount' => 1, + ], + ]); + $out->end(); + }; + $out->on('error', $handleError); + + /** @var FileAnalyser $fileAnalyser */ + $fileAnalyser = $container->getByType(FileAnalyser::class); + + /** @var Registry $registry */ + $registry = $container->getByType(Registry::class); + + // todo collectErrors (from Analyser) + $in->on('data', static function (array $json) use ($fileAnalyser, $registry, $out, $analysedFiles): void { + $action = $json['action']; + if ($action !== 'analyse') { + return; + } + + $internalErrorsCount = 0; + $files = $json['files']; + $errors = []; + $dependencies = []; + foreach ($files as $file) { + try { + $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $registry, null); + $fileErrors = $fileAnalyserResult->getErrors(); + $dependencies[$file] = $fileAnalyserResult->getDependencies(); + foreach ($fileErrors as $fileError) { + $errors[] = $fileError; + } + } catch (\Throwable $t) { + $internalErrorsCount++; + $internalErrorMessage = sprintf('Internal error: %s in file %s', $t->getMessage(), $file); + $internalErrorMessage .= sprintf( + '%sRun PHPStan with --debug option and post the stack trace to:%s%s', + "\n", + "\n", + 'https://github.com/phpstan/phpstan/issues/new' + ); + $errors[] = $internalErrorMessage; + } + } + + $out->write([ + 'action' => 'result', + 'result' => [ + 'errors' => $errors, + 'dependencies' => $dependencies, + 'filesCount' => count($files), + 'internalErrorsCount' => $internalErrorsCount, + ]]); + }); + $in->on('error', $handleError); + } + + public function haha(int $i): void + { + function () use ($i) { + + }; + } + + /** + * @param int $b + */ + public function doLorem(int $a): void + { + echo 'haha'; + return; + } + +}