From 3a491a9b340b8f5658e5f83a217a93ad156a0a1a Mon Sep 17 00:00:00 2001 From: Gawain Lynch Date: Mon, 6 Nov 2017 12:09:17 +0100 Subject: [PATCH 1/5] Add Syfmony 4.0 component constraints --- composer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 78fa537f6..ef2b400df 100644 --- a/composer.json +++ b/composer.json @@ -28,10 +28,10 @@ "composer/semver": "^1.0", "composer/spdx-licenses": "^1.0", "seld/jsonlint": "^1.4", - "symfony/console": "^2.7 || ^3.0", - "symfony/finder": "^2.7 || ^3.0", - "symfony/process": "^2.7 || ^3.0", - "symfony/filesystem": "^2.7 || ^3.0", + "symfony/console": "^2.7 || ^3.0 || ^4.0", + "symfony/finder": "^2.7 || ^3.0 || ^4.0", + "symfony/process": "^2.7 || ^3.0 || ^4.0", + "symfony/filesystem": "^2.7 || ^3.0 || ^4.0", "seld/phar-utils": "^1.0", "seld/cli-prompt": "^1.0", "psr/log": "^1.0" From 8b42aed060a6e57e088bb70bc84d85f34a05e637 Mon Sep 17 00:00:00 2001 From: Gawain Lynch Date: Mon, 6 Nov 2017 16:28:50 +0100 Subject: [PATCH 2/5] Create a local escapeArgument() for Symfony 4 compatibility. --- src/Composer/Util/ProcessExecutor.php | 57 ++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index 1074b2fce..408aa9eae 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -12,9 +12,9 @@ namespace Composer\Util; +use Composer\IO\IOInterface; use Symfony\Component\Process\Process; use Symfony\Component\Process\ProcessUtils; -use Composer\IO\IOInterface; /** * @author Robert Schönthal @@ -131,6 +131,59 @@ class ProcessExecutor */ public static function escape($argument) { - return ProcessUtils::escapeArgument($argument); + if (method_exists('Symfony\Component\Process\ProcessUtils', 'escapeArgument')) { + return ProcessUtils::escapeArgument($argument); + } + return self::escapeArgument($argument); + } + + /** + * Copy of ProcessUtils::escapeArgument() that is removed in Symfony 4. + * + * @param string $argument + * + * @return string + */ + private static function escapeArgument($argument) + { + //Fix for PHP bug #43784 escapeshellarg removes % from given string + //Fix for PHP bug #49446 escapeshellarg doesn't work on Windows + //@see https://bugs.php.net/bug.php?id=43784 + //@see https://bugs.php.net/bug.php?id=49446 + if ('\\' === DIRECTORY_SEPARATOR) { + if ('' === $argument) { + return escapeshellarg($argument); + } + + $escapedArgument = ''; + $quote = false; + foreach (preg_split('/(")/', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { + if ('"' === $part) { + $escapedArgument .= '\\"'; + } elseif (self::isSurroundedBy($part, '%')) { + // Avoid environment variable expansion + $escapedArgument .= '^%"'.substr($part, 1, -1).'"^%'; + } else { + // escape trailing backslash + if ('\\' === substr($part, -1)) { + $part .= '\\'; + } + $quote = true; + $escapedArgument .= $part; + } + } + if ($quote) { + $escapedArgument = '"'.$escapedArgument.'"'; + } + + return $escapedArgument; + } + + return "'".str_replace("'", "'\\''", $argument)."'"; + } + + private static function isSurroundedBy($arg, $char) + { + return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; } } From 157075b996a3858492a4871d07b309d8d7eccd21 Mon Sep 17 00:00:00 2001 From: Gawain Lynch Date: Mon, 6 Nov 2017 16:29:55 +0100 Subject: [PATCH 3/5] Migrate ConsoleIO::select to use QuestionHelper and ChoiceQuestion --- src/Composer/IO/ConsoleIO.php | 16 ++++++---- tests/Composer/Test/IO/ConsoleIOTest.php | 39 +++++++++++++----------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/Composer/IO/ConsoleIO.php b/src/Composer/IO/ConsoleIO.php index acb8a8564..f3a99edf2 100644 --- a/src/Composer/IO/ConsoleIO.php +++ b/src/Composer/IO/ConsoleIO.php @@ -12,11 +12,12 @@ namespace Composer\IO; +use Composer\Question\StrictConfirmationQuestion; +use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Helper\HelperSet; -use Composer\Question\StrictConfirmationQuestion; +use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; /** @@ -281,11 +282,14 @@ class ConsoleIO extends BaseIO */ public function select($question, $choices, $default, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false) { - if ($this->isInteractive()) { - return $this->helperSet->get('dialog')->select($this->getErrorOutput(), $question, $choices, $default, $attempts, $errorMessage, $multiselect); - } + /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ + $helper = $this->helperSet->get('question'); + $question = new ChoiceQuestion($question, $choices, $default); + $question->setMaxAttempts($attempts ?: null); // IOInterface requires false, and Question requires null or int + $question->setErrorMessage($errorMessage); + $question->setMultiselect($multiselect); - return $default; + return $helper->ask($this->input, $this->getErrorOutput(), $question); } /** diff --git a/tests/Composer/Test/IO/ConsoleIOTest.php b/tests/Composer/Test/IO/ConsoleIOTest.php index ff96a011f..518b6d0e1 100644 --- a/tests/Composer/Test/IO/ConsoleIOTest.php +++ b/tests/Composer/Test/IO/ConsoleIOTest.php @@ -229,27 +229,32 @@ class ConsoleIOTest extends TestCase { $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface'); $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); - $dialogMock = $this->getMock('Symfony\Component\Console\Helper\DialogHelper'); - $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet'); + $helperMock = $this->getMock('Symfony\Component\Console\Helper\QuestionHelper'); + $setMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet'); + + $helperMock + ->expects($this->once()) + ->method('ask') + ->with( + $this->isInstanceOf('Symfony\Component\Console\Input\InputInterface'), + $this->isInstanceOf('Symfony\Component\Console\Output\OutputInterface'), + $this->isInstanceOf('Symfony\Component\Console\Question\Question') + ) + ; + + $setMock + ->expects($this->once()) + ->method('get') + ->with($this->equalTo('question')) + ->will($this->returnValue($helperMock)) + ; $inputMock->expects($this->once()) ->method('isInteractive') - ->will($this->returnValue(true)); - $dialogMock->expects($this->once()) - ->method('select') - ->with($this->isInstanceOf('Symfony\Component\Console\Output\OutputInterface'), - $this->equalTo('Select item'), - $this->equalTo(array("item1", "item2")), - $this->equalTo(null), - $this->equalTo(false), - $this->equalTo("Error message"), - $this->equalTo(true)); - $helperMock->expects($this->once()) - ->method('get') - ->with($this->equalTo('dialog')) - ->will($this->returnValue($dialogMock)); + ->will($this->returnValue(true)) + ; - $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock); + $consoleIO = new ConsoleIO($inputMock, $outputMock, $setMock); $consoleIO->select('Select item', array("item1", "item2"), null, false, "Error message", true); } From f96e0e033b4098ce992d5c72eb0bb94ebc282f06 Mon Sep 17 00:00:00 2001 From: Gawain Lynch Date: Mon, 6 Nov 2017 16:30:43 +0100 Subject: [PATCH 4/5] Use Terminal class for dimensions post Symfony 3.2 --- src/Composer/Command/ShowCommand.php | 37 +++++++++++++++++----------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index e850b96d6..694e23874 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -12,32 +12,33 @@ namespace Composer\Command; -use Composer\DependencyResolver\Pool; +use Composer\Composer; use Composer\DependencyResolver\DefaultPolicy; +use Composer\DependencyResolver\Pool; use Composer\Json\JsonFile; -use Composer\Package\CompletePackageInterface; -use Composer\Package\Version\VersionParser; use Composer\Package\BasePackage; +use Composer\Package\CompletePackageInterface; +use Composer\Package\PackageInterface; +use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionSelector; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; -use Composer\Package\PackageInterface; +use Composer\Repository\ArrayRepository; +use Composer\Repository\ComposerRepository; +use Composer\Repository\CompositeRepository; +use Composer\Repository\PlatformRepository; +use Composer\Repository\RepositoryFactory; +use Composer\Repository\RepositoryInterface; use Composer\Semver\Constraint\ConstraintInterface; +use Composer\Semver\Semver; +use Composer\Spdx\SpdxLicenses; use Composer\Util\Platform; use Symfony\Component\Console\Formatter\OutputFormatterStyle; -use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Composer\Repository\ArrayRepository; -use Composer\Repository\CompositeRepository; -use Composer\Repository\ComposerRepository; -use Composer\Repository\PlatformRepository; -use Composer\Repository\RepositoryInterface; -use Composer\Repository\RepositoryFactory; -use Composer\Spdx\SpdxLicenses; -use Composer\Composer; -use Composer\Semver\Semver; +use Symfony\Component\Console\Terminal; /** * @author Robert Schönthal @@ -255,7 +256,13 @@ EOT $packageListFilter = $this->getRootRequires(); } - list($width) = $this->getApplication()->getTerminalDimensions(); + if (class_exists('Symfony\Component\Console\Terminal')) { + $terminal = new Terminal(); + $width = $terminal->getWidth(); + } else { + // For versions of Symfony console before 3.2 + list($width) = $this->getApplication()->getTerminalDimensions(); + } if (null === $width) { // In case the width is not detected, we're probably running the command // outside of a real terminal, use space without a limit From f74c6f462098e0149460d661ff50f3701e45694a Mon Sep 17 00:00:00 2001 From: Gawain Lynch Date: Mon, 6 Nov 2017 16:32:29 +0100 Subject: [PATCH 5/5] Update tests to handle optional QuestionHelper::setInputStream() availability --- tests/Composer/Test/IO/ConsoleIOTest.php | 5 --- .../StrictConfirmationQuestionTest.php | 43 +++++++++++-------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/tests/Composer/Test/IO/ConsoleIOTest.php b/tests/Composer/Test/IO/ConsoleIOTest.php index 518b6d0e1..183012b63 100644 --- a/tests/Composer/Test/IO/ConsoleIOTest.php +++ b/tests/Composer/Test/IO/ConsoleIOTest.php @@ -249,11 +249,6 @@ class ConsoleIOTest extends TestCase ->will($this->returnValue($helperMock)) ; - $inputMock->expects($this->once()) - ->method('isInteractive') - ->will($this->returnValue(true)) - ; - $consoleIO = new ConsoleIO($inputMock, $outputMock, $setMock); $consoleIO->select('Select item', array("item1", "item2"), null, false, "Error message", true); } diff --git a/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php b/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php index b0ad8c60a..a69d1aeef 100644 --- a/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php +++ b/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php @@ -16,6 +16,8 @@ use Composer\Question\StrictConfirmationQuestion; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\StreamableInputInterface; use Symfony\Component\Console\Output\StreamOutput; /** @@ -42,11 +44,11 @@ class StrictConfirmationQuestionTest extends TestCase */ public function testAskConfirmationBadAnswer($answer) { - $dialog = new QuestionHelper(); - $dialog->setInputStream($this->getInputStream($answer."\n")); + list($input, $dialog) = $this->createInput($answer."\n"); + $question = new StrictConfirmationQuestion('Do you like French fries?'); $question->setMaxAttempts(1); - $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question); + $dialog->ask($input, $this->createOutputInterface(), $question); } /** @@ -54,11 +56,10 @@ class StrictConfirmationQuestionTest extends TestCase */ public function testAskConfirmation($question, $expected, $default = true) { - $dialog = new QuestionHelper(); + list($input, $dialog) = $this->createInput($question."\n"); - $dialog->setInputStream($this->getInputStream($question."\n")); $question = new StrictConfirmationQuestion('Do you like French fries?', $default); - $this->assertEquals($expected, $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question), 'confirmation question should '.($expected ? 'pass' : 'cancel')); + $this->assertEquals($expected, $dialog->ask($input, $this->createOutputInterface(), $question), 'confirmation question should '.($expected ? 'pass' : 'cancel')); } public function getAskConfirmationData() @@ -75,13 +76,13 @@ class StrictConfirmationQuestionTest extends TestCase public function testAskConfirmationWithCustomTrueAndFalseAnswer() { - $dialog = new QuestionHelper(); - $question = new StrictConfirmationQuestion('Do you like French fries?', false, '/^ja$/i', '/^nein$/i'); - $dialog->setInputStream($this->getInputStream("ja\n")); - $this->assertTrue($dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); - $dialog->setInputStream($this->getInputStream("nein\n")); - $this->assertFalse($dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + + list($input, $dialog) = $this->createInput("ja\n"); + $this->assertTrue($dialog->ask($input, $this->createOutputInterface(), $question)); + + list($input, $dialog) = $this->createInput("nein\n"); + $this->assertFalse($dialog->ask($input, $this->createOutputInterface(), $question)); } protected function getInputStream($input) @@ -98,13 +99,19 @@ class StrictConfirmationQuestionTest extends TestCase return new StreamOutput(fopen('php://memory', 'r+', false)); } - protected function createInputInterfaceMock($interactive = true) + protected function createInput($entry) { - $mock = $this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(); - $mock->expects($this->any()) - ->method('isInteractive') - ->will($this->returnValue($interactive)); + $stream = $this->getInputStream($entry); + $input = new ArrayInput(array('--no-interaction')); + $dialog = new QuestionHelper(); - return $mock; + if (method_exists($dialog, 'setInputStream')) { + $dialog->setInputStream($stream); + } + if ($input instanceof StreamableInputInterface) { + $input->setStream($stream); + } + + return array($input, $dialog); } }