diff --git a/src/Feature/FeatureDetector.php b/src/Feature/FeatureDetector.php index 429102fe6..884611493 100644 --- a/src/Feature/FeatureDetector.php +++ b/src/Feature/FeatureDetector.php @@ -273,6 +273,11 @@ public function standardFeatures() ->checkInternalMethod('ReflectionParameter', 'isCallable'); }, + 'parameter.variadic' => function ($detector) { + return $detector + ->checkStatement('function (...$a) {};'); + }, + 'reflection.function.export.default.array' => function ($detector) { $function = new ReflectionFunction(function ($a0 = array('a')) {}); diff --git a/src/Mock/Generator/MockGenerator.php b/src/Mock/Generator/MockGenerator.php index 7d2a39811..2d6c5dc97 100644 --- a/src/Mock/Generator/MockGenerator.php +++ b/src/Mock/Generator/MockGenerator.php @@ -323,7 +323,7 @@ public static function __callStatic( $parameter[1] . '$a' . ++$index . - $parameter[2]; + $parameter[3]; } $source .= <<<'EOD' @@ -408,19 +408,24 @@ protected function generateMethods(array $methods) } $parameterCount = count($signature); + $variadic = false; if ($signature) { $argumentPacking = "\n"; $index = -1; foreach ($signature as $parameter) { - $argumentPacking .= "\n if (\$argumentCount > " . - ++$index . - ") {\n \$arguments[] = " . - $parameter[1] . - '$a' . - $index . - ";\n }"; + if ($parameter[2]) { + $parameterCount--; + } else { + $argumentPacking .= "\n if (\$argumentCount > " . + ++$index . + ") {\n \$arguments[] = " . + $parameter[1] . + '$a' . + $index . + ";\n }"; + } } } else { $argumentPacking = ''; @@ -475,9 +480,10 @@ protected function generateMethods(array $methods) $source .= $parameter[0] . $parameter[1] . + $parameter[2] . '$a' . ++$index . - $parameter[2]; + $parameter[3]; } $source .= "\n ) {\n"; diff --git a/src/Reflection/FunctionSignatureInspector.php b/src/Reflection/FunctionSignatureInspector.php index 246a630b7..0b08641ce 100644 --- a/src/Reflection/FunctionSignatureInspector.php +++ b/src/Reflection/FunctionSignatureInspector.php @@ -24,7 +24,7 @@ */ class FunctionSignatureInspector implements FunctionSignatureInspectorInterface { - const PARAMETER_PATTERN = '/^\s*Parameter #\d+ \[ <(required|optional)> (\S+ )?(?:or NULL )?(&)?\$(\S+)( = [^\]]+)? ]$/m'; + const PARAMETER_PATTERN = '/^\s*Parameter #\d+ \[ <(required|optional)> (\S+ )?(?:or NULL )?(&)?(?:\.\.\.)?\$(\S+)( = [^\]]+)? ]$/m'; /** * Get the static instance of this inspector. @@ -63,6 +63,8 @@ public function __construct( ->isSupported('reflection.function.export.default.array'); $this->isExportReferenceSupported = $featureDetector ->isSupported('reflection.function.export.reference'); + $this->isVariadicParameterSupported = $featureDetector + ->isSupported('parameter.variadic'); $this->isHhvm = $featureDetector->isSupported('runtime.hhvm'); } @@ -143,6 +145,14 @@ public function signature(ReflectionFunctionAbstract $function) $byReference = $parameter->isPassedByReference() ? '&' : ''; } // @codeCoverageIgnoreEnd + if ($this->isVariadicParameterSupported && $parameter->isVariadic()) { + $variadic = '...'; + $optional = false; + } else { + $variadic = ''; + $optional = 'optional' === $match[1]; + } + if (isset($match[5])) { if ( !$this->isExportDefaultArraySupported && @@ -164,14 +174,14 @@ public function signature(ReflectionFunctionAbstract $function) $defaultValue = str_replace('array (', 'array(', $defaultValue); } - } elseif ('optional' === $match[1]) { + } elseif ($optional) { $defaultValue = ' = null'; } else { $defaultValue = ''; } $signature[$match[4]] = - array($typehint, $byReference, $defaultValue); + array($typehint, $byReference, $variadic, $defaultValue); } return $signature; diff --git a/test/fixture/feature-detector/features.fixie.yml b/test/fixture/feature-detector/features.fixie.yml index e0b9ca8f0..24ba19ecd 100644 --- a/test/fixture/feature-detector/features.fixie.yml +++ b/test/fixture/feature-detector/features.fixie.yml @@ -22,6 +22,7 @@ data: Can yield no value: [generator.yield.nothing, '5.5.x', '999', [], '3.4.x', '999', [] ] Can use a constant as a parameter default value: [parameter.default.constant, '5.4.6.x', '999', [], '999', '0.x', [] ] Can type hint a parameter as callable: [parameter.type.callable, '5.4.x', '999', [], '0.x', '999', [] ] + Can use variadic parameters: [parameter.variadic, '5.6.x', '999', [], '3.3.x', '999', [] ] Reflection function exports include full array default value: [reflection.function.export.default.array, '999', '0.x', [], '3.2.x', '999', [] ] Reflection function exports include by-reference information: [reflection.function.export.reference, '0.x', '999', [], '3.4.x', '999', [] ] HHVM runtime: [runtime.hhvm, '999', '0.x', [], '0.x', '999', [] ] diff --git a/test/src/Test/TestInterfaceWithVariadicParameter.php b/test/src/Test/TestInterfaceWithVariadicParameter.php new file mode 100644 index 000000000..b9c423e3a --- /dev/null +++ b/test/src/Test/TestInterfaceWithVariadicParameter.php @@ -0,0 +1,17 @@ +assertNotInstanceOf(get_class($mockMock->mock()), $mock->mock()); } + public function testVariadicParameterMocking() + { + if (!$this->featureDetector->isSupported('parameter.variadic')) { + $this->markTestSkipped('Requires variadic parameters.'); + } + + $mock = x\mock('Eloquent\Phony\Test\TestInterfaceWithVariadicParameter'); + $mock->method->does( + function () { + return func_get_args(); + } + ); + + $this->assertSame( + array(1, 2, 3), + $mock->mock()->method(1, 2, 3) + ); + } + public function testSpyStatic() { $spy = Phony::spy(); diff --git a/test/suite/Reflection/FunctionSignatureInspectorTest.php b/test/suite/Reflection/FunctionSignatureInspectorTest.php index 5330ba8f8..6a20b4062 100644 --- a/test/suite/Reflection/FunctionSignatureInspectorTest.php +++ b/test/suite/Reflection/FunctionSignatureInspectorTest.php @@ -67,22 +67,22 @@ function ( ); $actual = $this->subject->signature($function); $expected = array( - 'a' => array('', '', ''), - 'b' => array('', '&', ''), - 'c' => array('array ', '', ''), - 'd' => array('array ', '&', ''), - 'e' => array('\Type ', '', ''), - 'f' => array('\Type ', '&', ''), - 'g' => array('\Namespaced\Type ', '', ''), - 'h' => array('\Namespaced\Type ', '&', ''), - 'i' => array('\Eloquent\Phony\Feature\FeatureDetector ', '', ''), - 'j' => array('', '', " = 'string'"), - 'k' => array('', '&', ' = 111'), - 'm' => array('array ', '&', ' = null'), - 'n' => array('\Type ', '', ' = null'), - 'o' => array('\Type ', '&', ' = null'), - 'p' => array('\Namespaced\Type ', '', ' = null'), - 'q' => array('\Namespaced\Type ', '&', ' = null'), + 'a' => array('', '', '', ''), + 'b' => array('', '&', '', ''), + 'c' => array('array ', '', '', ''), + 'd' => array('array ', '&', '', ''), + 'e' => array('\Type ', '', '', ''), + 'f' => array('\Type ', '&', '', ''), + 'g' => array('\Namespaced\Type ', '', '', ''), + 'h' => array('\Namespaced\Type ', '&', '', ''), + 'i' => array('\Eloquent\Phony\Feature\FeatureDetector ', '', '', ''), + 'j' => array('', '', '', " = 'string'"), + 'k' => array('', '&', '', ' = 111'), + 'm' => array('array ', '&', '', ' = null'), + 'n' => array('\Type ', '', '', ' = null'), + 'o' => array('\Type ', '&', '', ' = null'), + 'p' => array('\Namespaced\Type ', '', '', ' = null'), + 'q' => array('\Namespaced\Type ', '&', '', ' = null'), ); $this->assertEquals($expected, $actual); @@ -105,7 +105,7 @@ public function testSignatureWithArrayDefault() $actual = $this->subject->signature($function); $this->assertArrayHasKey('a', $actual); - $this->assertSame(array('a', 'b', 'c' => 'd'), eval('return $r' . $actual['a'][2] . ';')); + $this->assertSame(array('a', 'b', 'c' => 'd'), eval('return $r' . $actual['a'][3] . ';')); } public function testSignatureWithUnavailableDefaultValue() @@ -113,7 +113,7 @@ public function testSignatureWithUnavailableDefaultValue() $function = new ReflectionMethod('ReflectionClass', 'getMethods'); $actual = $this->subject->signature($function); $expected = array( - 'filter' => array('', '', ' = null'), + 'filter' => array('', '', '', ' = null'), ); $this->assertEquals($expected, $actual); @@ -131,8 +131,8 @@ public function testSignatureWithCallableTypeHint() ); $actual = $this->subject->signature($function); $expected = array( - 'a' => array('callable ', '', ''), - 'b' => array('callable ', '', ' = null'), + 'a' => array('callable ', '', '', ''), + 'b' => array('callable ', '', '', ' = null'), ); $this->assertEquals($expected, $actual); @@ -148,8 +148,8 @@ public function testSignatureWithConstantDefault() $function = new ReflectionMethod($this, 'methodA'); $actual = $this->subject->signature($function); $expected = array( - 'a' => array('', '', ' = 4'), - 'b' => array('', '', " = 'a'"), + 'a' => array('', '', '', ' = 4'), + 'b' => array('', '', '', " = 'a'"), ); $this->assertEquals($expected, $actual); @@ -161,7 +161,7 @@ public function testSignatureWithSelfTypeHint() $function = new ReflectionMethod($this, 'methodB'); $actual = $this->subject->signature($function); $expected = array( - 'a' => array('\Eloquent\Phony\Reflection\FunctionSignatureInspectorTest ', '', ''), + 'a' => array('\Eloquent\Phony\Reflection\FunctionSignatureInspectorTest ', '', '', ''), ); $this->assertEquals($expected, $actual); @@ -172,14 +172,33 @@ public function testCallbackSignature() { $callback = function ($a, array $b = null) {}; $expected = array( - 'a' => array('', '', ''), - 'b' => array('array ', '', ' = null'), + 'a' => array('', '', '', ''), + 'b' => array('array ', '', '', ' = null'), ); $actual = $this->subject->callbackSignature($callback); $this->assertSame($actual, $expected); } + public function testSignatureWithVariadicParameter() + { + if (!$this->featureDetector->isSupported('parameter.variadic')) { + $this->markTestSkipped('Requires variadic parameters.'); + } + + $code = 'return function (...$a) { return $args; };'; + $callback = eval($code); + $function = new ReflectionFunction($callback); + + $actual = $this->subject->signature($function); + $expected = array( + 'a' => array('', '', '...', ''), + ); + + $this->assertEquals($expected, $actual); + $this->assertSame($expected, $actual); + } + protected function methodA($a = ReflectionMethod::IS_FINAL, $b = self::CONSTANT_A) { } diff --git a/test/suite/Stub/StubTest.php b/test/suite/Stub/StubTest.php index 668820edb..66f0ad914 100644 --- a/test/suite/Stub/StubTest.php +++ b/test/suite/Stub/StubTest.php @@ -1096,4 +1096,22 @@ public function testInvokeWithWithReferenceParameters() $this->assertSame('c', $c); $this->assertSame('d', $d); } + + public function testStubWithVariadicParameter() + { + if (!$this->featureDetector->isSupported('parameter.variadic')) { + $this->markTestSkipped('Requires variadic parameters.'); + } + + $code = 'return function (...$args) { return $args; };'; + $callback = eval($code); + + $this->subject = new Stub($callback); + $this->subject->forwards(); + + $this->assertSame( + array(1, 2, 3), + call_user_func($this->subject, 1, 2, 3) + ); + } }