Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES-v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Fixed
- Nullable parameter typehints (`?Foo`) in spec classes no longer produce a `ParseError` when the spec is loaded [#1583](https://github.com/phpspec/phpspec/issues/1583)

## [8.3.1]
### Fixed
- Bump `phpspec/prophecy` constraint to `^1.26.1` to fix compatibility issues that were breaking CI and had prevented the PHAR from being published with the 8.3.0 release
Expand Down
69 changes: 69 additions & 0 deletions features/doubles/nullable_doubles.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
Feature: Nullable Doubles

Collaborators with a nullable typehint are doubled using the underlying type

Scenario: Creating a double from a nullable typehint
Given the spec file "spec/Doubles/NullableDouble/HandlerSpec.php" contains:
"""
<?php

namespace spec\Doubles\NullableDouble;

use PhpSpec\ObjectBehavior;

class HandlerSpec extends ObjectBehavior
{
function it_creates_a_double_from_a_nullable_typehint(?\ArrayObject $double)
{
if (!$double->getWrappedObject() instanceof \ArrayObject) {
throw new \Exception('The nullable collaborator was not doubled as ArrayObject');
}
}
}
"""
And the class file "src/Doubles/NullableDouble/Handler.php" contains:
"""
<?php

namespace Doubles\NullableDouble;

class Handler
{
}
"""
When I run phpspec
Then the suite should pass

Scenario: Loading a spec that has a nullable typehint on a private helper method
Given the spec file "spec/Doubles/NullableHelper/CalendarSpec.php" contains:
"""
<?php

namespace spec\Doubles\NullableHelper;

use PhpSpec\ObjectBehavior;

class CalendarSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(\Doubles\NullableHelper\Calendar::class);
}

private function helper(?\DateTimeImmutable $date = null): void
{
}
}
"""
And the class file "src/Doubles/NullableHelper/Calendar.php" contains:
"""
<?php

namespace Doubles\NullableHelper;

class Calendar
{
}
"""
When I run phpspec
Then the suite should pass
123 changes: 123 additions & 0 deletions spec/PhpSpec/CodeAnalysis/TokenizedTypeHintRewriterSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -365,4 +365,127 @@ public function bar(Bar | Baz $bar)
Argument::type(DisallowedUnionTypehintException::class)
)->shouldHaveBeenCalled();
}

function it_removes_nullable_typehints()
{
$this->rewrite('
<?php

class FooSpec
{
public function bar(?Bar $bar)
{
}
}

')->shouldReturn('
<?php

class FooSpec
{
public function bar( $bar)
{
}
}

');
}

function it_removes_nullable_fully_qualified_typehints()
{
$this->rewrite('
<?php

class FooSpec
{
public function bar(?\Foo\Bar $bar)
{
}
}

')->shouldReturn('
<?php

class FooSpec
{
public function bar( $bar)
{
}
}

');
}

function it_removes_nullable_typehints_with_whitespace()
{
$this->rewrite('
<?php

class FooSpec
{
public function bar(? Bar $bar)
{
}
}

')->shouldReturn('
<?php

class FooSpec
{
public function bar( $bar)
{
}
}

');
}

function it_indexes_nullable_typehints_without_the_nullable_marker(TypeHintIndex $typeHintIndex, NamespaceResolver $namespaceResolver)
{
$namespaceResolver->analyse(Argument::any())->shouldBeCalled();

$namespaceResolver->resolve('FooSpec')->willReturn('FooSpec');
$namespaceResolver->resolve('Bar')->willReturn('Bar');

$this->rewrite('
<?php

class FooSpec
{
public function bar(?Bar $bar)
{
}
}

');

$typeHintIndex->add('FooSpec', 'bar', '$bar', 'Bar')->shouldHaveBeenCalled();
}

function it_indexes_invalid_nullable_scalar_typehints(
TypeHintIndex $typeHintIndex,
NamespaceResolver $namespaceResolver
) {
$e = new DisallowedNonObjectTypehintException();
$namespaceResolver->analyse(Argument::any())->shouldBeCalled();

$namespaceResolver->resolve('FooSpec')->willReturn('FooSpec');
$namespaceResolver->resolve('int')->willThrow($e);

$this->rewrite('
<?php

class FooSpec
{
public function bar(?int $bar)
{
}
}

');

$typeHintIndex->addInvalid('FooSpec', 'bar', '$bar', $e)->shouldHaveBeenCalled();
$typeHintIndex->add('FooSpec', 'bar', '$bar', Argument::any())->shouldNotHaveBeenCalled();
}
}
14 changes: 9 additions & 5 deletions src/PhpSpec/CodeAnalysis/TokenizedTypeHintRewriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ private function stripTypeHints(array $tokens): array
private function extractTypehints(array &$tokens, int $variableNameIndex, Token $variableName): void
{
$typehint = '';
for ($i = $variableNameIndex - 1; !$this->haveNotReachedEndOfTypeHint($tokens[$i]); $i--) {
for ($i = $variableNameIndex - 1; $this->isPartOfTypeHint($tokens[$i]); $i--) {
$scanningToken = $tokens[$i];
$typehint = $scanningToken->asString() . $typehint;

Expand All @@ -143,6 +143,10 @@ private function extractTypehints(array &$tokens, int $variableNameIndex, Token

if ($typehint = trim($typehint)) {

if (\str_starts_with($typehint, '?')) {
$typehint = ltrim(substr($typehint, 1));
}

$class = $this->namespaceResolver->resolve($this->currentClass);

if (\strpos($typehint, '|') !== false) {
Expand Down Expand Up @@ -186,13 +190,13 @@ private function extractTypehints(array &$tokens, int $variableNameIndex, Token
}
}

private function haveNotReachedEndOfTypeHint(Token $token) : bool
private function isPartOfTypeHint(Token $token): bool
{
if ($token->equals('|') || $token->equals('&')) {
return false;
if ($token->equals('|') || $token->equals('&') || $token->equals('?')) {
return true;
}

return !$token->isInTypes($this->typehintTokens);
return $token->isInTypes($this->typehintTokens);
}

private function shouldExtractTokensOfClass(string $className): bool
Expand Down
Loading