Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
m0003r committed Apr 27, 2021
1 parent eecc05f commit b8ce39d
Show file tree
Hide file tree
Showing 2 changed files with 255 additions and 14 deletions.
18 changes: 4 additions & 14 deletions src/Checker/PsrContainerChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
use Psalm\Type\Union;
use Psr\Container\ContainerInterface;

use function count;
use function explode;
use function is_string;

Expand Down Expand Up @@ -76,7 +75,7 @@ public static function afterMethodCallAnalysis(
}

$candidate = self::handleVariable($variableType);
if ($candidate !== null) {
if (! $candidate->isMixed()) {
$return_type_candidate = $candidate;
}

Expand All @@ -96,9 +95,8 @@ public static function afterMethodCallAnalysis(
}

// phpcs:disable Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
protected static function handleVariable(Union $variableType): ?Union
private static function handleVariable(Union $variableType): Union
{
$hasMixed = false;
/** @var list<Atomic> $types */
$types = [];
foreach ($variableType->getAtomicTypes() as $type) {
Expand All @@ -111,18 +109,10 @@ protected static function handleVariable(Union $variableType): ?Union
} elseif ($type instanceof TClassString && $type->as_type !== null) {
$types[] = $type->as_type;
} else {
if (! $hasMixed) {
$types[] = new Atomic\TMixed();
}

$hasMixed = true;
$types[] = new Atomic\TMixed();
}
}

if (count($types) > 0) {
return new Union($types);
}

return null;
return new Union($types);
}
}
251 changes: 251 additions & 0 deletions test/Unit/Checker/PsrContainerCheckerClassStringTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
<?php

declare(strict_types=1);

namespace Lctrs\PsalmPsrContainerPlugin\Test\Unit\Checker;

use Lctrs\PsalmPsrContainerPlugin\Checker\PsrContainerChecker;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PHPUnit\Framework\MockObject\Stub;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\StatementsSource;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Union;
use Psr\Container\ContainerInterface;

class PsrContainerCheckerClassStringTest extends TestCase
{
use ProphecyTrait;

private const METHOD_ID = 'Psr\Container\ContainerInterface::get';
private const VARIABLE_NAME = 'param';

public function testItDoesNothingWithEmptyContext(): void
{
$fileReplacements = [];
$returnTypeCandidate = $baseReturnType = new Union([new TMixed()]);

PsrContainerChecker::afterMethodCallAnalysis(
$this->getMethodCall(),
self::METHOD_ID,
self::METHOD_ID,
self::METHOD_ID,
$this->createStub(Context::class),
$this->createStub(StatementsSource::class),
$this->createStub(Codebase::class),
$fileReplacements,
$returnTypeCandidate
);

self::assertSame($baseReturnType, $returnTypeCandidate);
}

/**
* @dataProvider pairsProvider
*/
public function testItSetsTheReturnTypeAsAUnionWithFetchedClass(Union $variableType, Union $expectedType): void
{
$fileReplacements = [];
$returnTypeCandidate = new Union([new TMixed()]);

PsrContainerChecker::afterMethodCallAnalysis(
$this->getMethodCall(),
self::METHOD_ID,
self::METHOD_ID,
self::METHOD_ID,
$this->createContext($variableType),
$this->createStub(StatementsSource::class),
$this->createStub(Codebase::class),
$fileReplacements,
$returnTypeCandidate
);

self::assertNotNull($returnTypeCandidate);
self::assertTrue($expectedType->equals($returnTypeCandidate));

if (! $expectedType->hasTemplate()) {
return;
}

// we should also check for template match
self::assertEquals($expectedType->getId(), $returnTypeCandidate->getId());
}

/**
* @dataProvider pairsProvider
*/
public function testItSetsTheReturnTypeAsAUnionWithFetchedClassWithContainerImplementingContainerInterface(
Union $variableType,
Union $expectedType
): void {
$fileReplacements = [];
$returnTypeCandidate = new Union([new TMixed()]);

$codebase = $this->prophesize(Codebase::class);
$codebase->classImplements(MyOtherContainer::class, ContainerInterface::class)
->willReturn(true)
->shouldBeCalledOnce();

PsrContainerChecker::afterMethodCallAnalysis(
$this->getMethodCall(),
MyOtherContainer::class . '::get',
MyOtherContainer::class . '::get',
MyOtherContainer::class . '::get',
$this->createContext($variableType),
$this->createStub(StatementsSource::class),
$codebase->reveal(),
$fileReplacements,
$returnTypeCandidate
);

self::assertNotNull($returnTypeCandidate);
self::assertTrue($expectedType->equals($returnTypeCandidate));

if (! $expectedType->hasTemplate()) {
return;
}

// we should also check for template match
self::assertEquals($expectedType->getId(), $returnTypeCandidate->getId());
}

/**
* @return array<string, array{0: Union, 1: Union}>
*/
public function pairsProvider(): array
{
return [
'mixed variable' => [
new Union([new TMixed()]),
new Union([new TMixed()]),
],
'class string' => [
new Union([
new TClassString('object', new TNamedObject('Abracadabra')),
]),
new Union([new TNamedObject('Abracadabra')]),
],
'class string and mixed' => [
new Union([
new TClassString('object', new TNamedObject('Abracadabra')),
new TMixed(),
]),
new Union([
new TNamedObject('Abracadabra'),
new TMixed(),
]),
],
'templated class string without as_type' => [
new Union([
new TTemplateParamClass(
'T',
'object',
null,
'definingclass'
),
]),
new Union([
new TTemplateParam(
'T',
new Union([new TObject()]),
'definingclass'
),
]),
],
'templated class string with as_type' => [
new Union([
new TTemplateParamClass(
'T',
'Abracadabra',
new TNamedObject('Abracadabra'),
'definingclass'
),
]),
new Union([
new TTemplateParam(
'T',
new Union([new TNamedObject('Abracadabra')]),
'definingclass'
),
]),
],
'union of class string or templated class string' => [
new Union([
new TTemplateParamClass(
'T',
'object',
null,
'definingclass'
),
new TClassString('object', new TNamedObject('Abracadabra')),
]),
new Union([
new TTemplateParam(
'T',
new Union([new TObject()]),
'definingclass'
),
new TNamedObject('Abracadabra'),
]),
],
];
}

/**
* @return Stub&Context
*/
protected function createContext(Union $variableType)
{
$stub = $this->createStub(Context::class);

// phpcs:disable Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$stub->vars_in_scope['$' . self::VARIABLE_NAME] = $variableType;

return $stub;
}

protected function getMethodCall(): MethodCall
{
return new MethodCall(
new Variable('dummy'),
'get',
[
new Arg(
new Variable(self::VARIABLE_NAME)
),
]
);
}
}


// phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses,Squiz.Classes.ClassFileName.NoMatch

final class MyOtherContainer implements ContainerInterface
{
/**
* @inheritDoc
*/
public function get($id)
{
return 'dummy';
}

/**
* @inheritDoc
*/
public function has($id)
{
return true;
}
}

0 comments on commit b8ce39d

Please sign in to comment.