diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 87eacf1..e9683dc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,5 +17,8 @@ jobs:
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- - name: PHPStan
- run: vendor/bin/phpstan analyze
+ - name: Install Psalm
+ run: composer require --dev vimeo/psalm:dev-master@dev --no-suggest
+
+ - name: Psalm
+ run: vendor/bin/psalm --output-format=github --shepherd
diff --git a/README.md b/README.md
index defbfb2..3370fc4 100644
--- a/README.md
+++ b/README.md
@@ -92,3 +92,7 @@ features and integration tests. Special thanks goes to
[@lunika](https://github.com/lunika), [@rgazelot](https://github.com/rgazelot)
and [@krichprollsch](https://github.com/krichprollsch), who helped conceived
this extension, and also pushed me to open-source it.
+
+Badges
+------
+
diff --git a/composer.json b/composer.json
index 666de43..e1d1388 100644
--- a/composer.json
+++ b/composer.json
@@ -62,8 +62,6 @@
"symfony/http-client": "^4.3 || ^5.0",
"symfony/var-dumper": "^2.8 || ^3.3 || ^4.0",
- "phpstan/phpstan": "^0.10",
- "phpstan/phpstan-strict-rules": "^0.10",
- "phpstan/phpstan-beberlei-assert": "^0.10"
+ "vimeo/psalm": "^3.8"
}
}
diff --git a/phpstan.neon b/phpstan.neon
deleted file mode 100644
index 87e21fa..0000000
--- a/phpstan.neon
+++ /dev/null
@@ -1,19 +0,0 @@
-parameters:
- level: max
- paths:
- - src
-
- ignoreErrors:
- # have to ignore it, because Symfony config works like that and I'm waaay
- # too lazy to properly structure the node builders just to add some asserts
- # and stuff like that for (almost) every line.
- - '{^Cannot call method arrayNode\\() on Symfony\Component\Config\Definition\Builder\NodeParentInterface|null.$}'
-
- # have to ignore it until php 7.4, which introduces (co)vrariance on arguments
- # and return hint, as beberlei/assert introduced a tiny bc break making it
- # impossible to properly extend its classes until php 7.4
- - '{^Call to an undefined method Assert\\AssertionChain::not().}'
-
- includes:
- - vendor/phpstan/phpstan-strict-rules/rules.neon
- - vendor/phpstan/phpstan-beberlei-assert/extension.neon
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..e2cedae
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Assert/Assertion.php b/src/Assert/Assertion.php
index 4ded19b..05a2318 100644
--- a/src/Assert/Assertion.php
+++ b/src/Assert/Assertion.php
@@ -15,8 +15,12 @@
*/
abstract class Assertion extends Beberlei\Assertion
{
- /** @return bool */
- public static function empty($value, $message = null, ?string $propertyPath = null): bool
+ /**
+ * @param mixed $value
+ * @param string|null $message
+ * @return bool
+ */
+ public static function empty($value, ?string $message = null, ?string $propertyPath = null): bool
{
return parent::noContent($value, $message, $propertyPath);
}
diff --git a/src/Behapi.php b/src/Behapi.php
index 0c515df..bd811d0 100644
--- a/src/Behapi.php
+++ b/src/Behapi.php
@@ -24,14 +24,16 @@ final class Behapi implements Extension
{
const DEBUG_INTROSPECTION_TAG = 'behapi.debug.introspection';
- /** {@inheritDoc} */
- public function getConfigKey()
+ public function getConfigKey(): string
{
return 'behapi';
}
- /** {@inheritDoc} */
- public function configure(ArrayNodeDefinition $builder)
+ /**
+ * @psalm-suppress PossiblyUndefinedMethod
+ * @psalm-suppress PossiblyNullReference
+ */
+ public function configure(ArrayNodeDefinition $builder): void
{
$builder
->children()
@@ -89,13 +91,11 @@ public function configure(ArrayNodeDefinition $builder)
}
- /** {@inheritDoc} */
- public function initialize(ExtensionManager $extensionManager)
+ public function initialize(ExtensionManager $extensionManager): void
{
}
- /** {@inheritDoc} */
- public function load(ContainerBuilder $container, array $config)
+ public function load(ContainerBuilder $container, array $config): void
{
$container->register(HttpHistory\History::class, HttpHistory\History::class)
->setPublic(false)
@@ -112,8 +112,7 @@ public function load(ContainerBuilder $container, array $config)
$this->loadContainer($container, $config);
}
- /** {@inheritDoc} */
- public function process(ContainerBuilder $container)
+ public function process(ContainerBuilder $container): void
{
$dumpers = [];
diff --git a/src/Container.php b/src/Container.php
index 8aa7b42..fea61c7 100644
--- a/src/Container.php
+++ b/src/Container.php
@@ -72,12 +72,13 @@ private function getPluginClientBuilder(): PluginClientBuilder
$uriFactory = Psr17FactoryDiscovery::findUrlFactory();
$baseUri = $uriFactory->createUri($this->baseUrl);
+ $httpHistory = $this->services[HttpHistory::class];
- assert($this->services[HttpHistory::class] instanceof HttpHistory);
+ assert($httpHistory instanceof HttpHistory);
$builder = $builder->addPlugin(new ContentLengthPlugin);
$builder = $builder->addPlugin(new BaseUriPlugin($baseUri));
- $builder = $builder->addPlugin(new HistoryPlugin($this->services[HttpHistory::class]));
+ $builder = $builder->addPlugin(new HistoryPlugin($httpHistory));
return $builder;
}
diff --git a/src/Debug/CliController.php b/src/Debug/CliController.php
index 911b3f8..d24576f 100644
--- a/src/Debug/CliController.php
+++ b/src/Debug/CliController.php
@@ -19,19 +19,19 @@ public function __construct(Status $status)
$this->status = $status;
}
- /** {@inheritDoc} */
- public function configure(Command $command)
+ public function configure(Command $command): void
{
$command
->addOption('behapi-debug', null, InputOption::VALUE_NONE, 'Activates the debug mode for behapi');
}
- /** {@inheritDoc} */
public function execute(InputInterface $input, OutputInterface $output)
{
$debug = $input->getOption('behapi-debug');
assert(is_bool($debug));
$this->status->setEnabled($debug);
+
+ return null;
}
}
diff --git a/src/Debug/Listener.php b/src/Debug/Listener.php
index 7463050..28df9de 100644
--- a/src/Debug/Listener.php
+++ b/src/Debug/Listener.php
@@ -48,8 +48,7 @@ public function __construct(Status $status, HttpHistory $history, array $adapter
$this->status = $status;
}
- /** {@inheritDoc} */
- public static function getSubscribedEvents()
+ public static function getSubscribedEvents(): array
{
return [
ExampleTested::AFTER => 'debugAfter',
@@ -101,6 +100,11 @@ private function debug(MessageInterface $message): void
}
}
+ /**
+ * @psalm-suppress InvalidReturnType
+ * @psalm-suppress InvalidReturnStatement
+ * @psalm-suppress UndefinedDocblockClass
+ */
private function hasTag(ScenarioLikeTested $event, string $tag): bool
{
$node = $event->getNode();
diff --git a/src/Debug/Status.php b/src/Debug/Status.php
index 6a768bf..89432a0 100644
--- a/src/Debug/Status.php
+++ b/src/Debug/Status.php
@@ -11,7 +11,7 @@ final class Status
/** @var bool */
private $enabled = false;
- public function setEnabled(bool $enabled)
+ public function setEnabled(bool $enabled): void
{
$this->enabled = $enabled;
}
diff --git a/src/HttpHistory/History.php b/src/HttpHistory/History.php
index ea79b7f..bbf0cce 100644
--- a/src/HttpHistory/History.php
+++ b/src/HttpHistory/History.php
@@ -53,6 +53,7 @@ public function getLastResponse(): ResponseInterface
return $response;
}
+ /** @return Generator */
public function getTuples(): Generator
{
yield from $this->tuples;
diff --git a/src/HttpHistory/Listener.php b/src/HttpHistory/Listener.php
index c941e9a..6af7b95 100644
--- a/src/HttpHistory/Listener.php
+++ b/src/HttpHistory/Listener.php
@@ -22,8 +22,7 @@ public function __construct(History $history)
$this->history = $history;
}
- /** {@inheritDoc} */
- public static function getSubscribedEvents()
+ public static function getSubscribedEvents(): array
{
return [
ExampleTested::AFTER => ['clear', -99],
diff --git a/src/Json/Context.php b/src/Json/Context.php
index a08e9e4..7852ca7 100644
--- a/src/Json/Context.php
+++ b/src/Json/Context.php
@@ -13,6 +13,7 @@
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Behapi\Assert\Assert;
+use Behapi\Assert\AssertionChain;
use Behapi\HttpHistory\History as HttpHistory;
use function sprintf;
@@ -32,12 +33,13 @@ class Context implements BehatContext
/** @var HttpHistory */
private $history;
- /** @var string[] */
+ /** @var list */
private $contentTypes;
/** @var PropertyAccessor */
private $accessor;
+ /** @param list $contentTypes */
public function __construct(HttpHistory $history, array $contentTypes = ['application/json'])
{
$this->accessor = PropertyAccess::createPropertyAccessor();
@@ -63,6 +65,7 @@ final public function path_should_be_readable(string $path, ?string $not = null)
$assert = Assert::that($this->accessor->isReadable($this->getValue(null), $path));
if ($not !== null) {
+ assert($assert instanceof AssertionChain);
$assert = $assert->not();
}
@@ -70,6 +73,8 @@ final public function path_should_be_readable(string $path, ?string $not = null)
}
/**
+ * @param mixed $expected
+ *
* @Then in the json, :path should be equal to :expected
* @Then in the json, :path should :not be equal to :expected
*/
@@ -78,6 +83,7 @@ final public function the_json_path_should_be_equal_to(string $path, ?string $no
$assert = Assert::that($this->getValue($path));
if ($not !== null) {
+ assert($assert instanceof AssertionChain);
$assert = $assert->not();
}
@@ -93,6 +99,7 @@ final public function the_json_path_should_be_py_string(string $path, ?string $n
$assert = Assert::that($this->getValue($path));
if ($not !== null) {
+ assert($assert instanceof AssertionChain);
$assert = $assert->not();
}
@@ -108,6 +115,7 @@ final public function the_json_path_should_be(string $path, ?string $not = null,
$assert = Assert::that($this->getValue($path));
if ($not !== null) {
+ assert($assert instanceof AssertionChain);
$assert = $assert->not();
}
@@ -123,6 +131,7 @@ final public function the_json_path_should_be_null(string $path, ?string $not =
$assert = Assert::that($this->getValue($path));
if ($not !== null) {
+ assert($assert instanceof AssertionChain);
$assert = $assert->not();
}
@@ -136,6 +145,7 @@ final public function the_json_path_should_be_null(string $path, ?string $not =
final public function the_json_path_should_be_empty(string $path, ?string $not = null): void
{
$assert = Assert::that($this->getValue($path));
+ assert($assert instanceof AssertionChain);
if ($not !== null) {
$assert = $assert->not();
@@ -145,6 +155,8 @@ final public function the_json_path_should_be_empty(string $path, ?string $not =
}
/**
+ * @param mixed $expected
+ *
* @Then in the json, :path should contain :expected
* @Then in the json, :path should :not contain :expected
*/
@@ -153,13 +165,18 @@ final public function the_json_path_contains(string $path, ?string $not = null,
$assert = Assert::that($this->getValue($path));
if ($not !== null) {
+ assert($assert instanceof AssertionChain);
$assert = $assert->not();
}
$assert->contains($expected);
}
- /** @Then in the json, :path collection should contain an element with :value equal to :expected */
+ /**
+ * @param mixed $expected
+ *
+ * @Then in the json, :path collection should contain an element with :value equal to :expected
+ */
final public function the_json_path_collection_contains(string $path, string $value, $expected): void
{
$collection = $this->getValue($path);
@@ -215,7 +232,7 @@ final public function the_json_path_should_be_a_valid_json_encoded_string(string
* overwrite it if you want to add supplementary checks or use something
* else instead (such as Seldaek's JsonLint package).
*/
- public function response_should_be_a_valid_json_response()
+ public function response_should_be_a_valid_json_response(): void
{
$this->getValue(null);
@@ -239,6 +256,7 @@ final public function the_json_path_should_match(string $path, ?string $not = nu
$assert = Assert::that($this->getValue($path));
if ($not !== null) {
+ assert($assert instanceof AssertionChain);
$assert = $assert->not();
}
diff --git a/src/Json/EachInCollectionTrait.php b/src/Json/EachInCollectionTrait.php
index ccdce6a..f679727 100644
--- a/src/Json/EachInCollectionTrait.php
+++ b/src/Json/EachInCollectionTrait.php
@@ -4,6 +4,7 @@
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Behapi\Assert\Assert;
+use Behapi\Assert\AssertionChain;
trait EachInCollectionTrait
{
@@ -21,6 +22,7 @@ final public function the_json_path_elements_in_collection_should_have_a_propert
$assert = Assert::that($this->getValue(empty($path) ? null : $path));
if ($not !== null) {
+ assert($assert instanceof AssertionChain);
$assert = $assert->not();
}
@@ -42,13 +44,23 @@ final public function the_json_each_elements_in_collection_should_be_equal_to(st
->isTraversable()
;
- $values = array_map(function ($value) use ($property) { return $this->accessor->getValue($value, $property); }, $values);
+ $values = array_map(
+ /**
+ * @param mixed $value
+ * @return mixed
+ */
+ function ($value) use ($property) {
+ return $this->accessor->getValue($value, $property);
+ },
+ $values
+ );
$assert = Assert::that($values)
->all()
;
if ($not !== null) {
+ assert($assert instanceof AssertionChain);
$assert = $assert->not();
}
@@ -67,7 +79,16 @@ final public function the_json_each_elements_in_collection_should_be_bool(string
->isTraversable()
;
- $values = array_map(function ($value) use ($property) { return $this->accessor->getValue($value, $property); }, $values);
+ $values = array_map(
+ /**
+ * @param mixed $value
+ * @return mixed
+ */
+ function ($value) use ($property) {
+ return $this->accessor->getValue($value, $property);
+ },
+ $values
+ );
$assert = Assert::that($values)
->all()
@@ -75,6 +96,7 @@ final public function the_json_each_elements_in_collection_should_be_bool(string
;
if ($not !== null) {
+ assert($assert instanceof AssertionChain);
$assert = $assert->not();
}
@@ -93,7 +115,16 @@ final public function the_json_each_elements_in_collection_should_be_equal_to_in
->isTraversable()
;
- $values = array_map(function ($value) use ($property) { return $this->accessor->getValue($value, $property); }, $values);
+ $values = array_map(
+ /**
+ * @param mixed $value
+ * @return mixed
+ */
+ function ($value) use ($property) {
+ return $this->accessor->getValue($value, $property);
+ },
+ $values
+ );
$assert = Assert::that($values)
->all()
@@ -101,6 +132,7 @@ final public function the_json_each_elements_in_collection_should_be_equal_to_in
;
if ($not !== null) {
+ assert($assert instanceof AssertionChain);
$assert = $assert->not();
}
@@ -119,13 +151,23 @@ final public function the_json_each_elements_in_collection_should_contain(string
->isTraversable()
;
- $values = array_map(function ($value) use ($property) { return $this->accessor->getValue($value, $property); }, $values);
+ $values = array_map(
+ /**
+ * @param mixed $value
+ * @return mixed
+ */
+ function ($value) use ($property) {
+ return $this->accessor->getValue($value, $property);
+ },
+ $values
+ );
$assert = Assert::that($values)
->all()
;
if ($not !== null) {
+ assert($assert instanceof AssertionChain);
$assert = $assert->not();
}
diff --git a/src/PhpMatcher/JsonContext.php b/src/PhpMatcher/JsonContext.php
index dfcafc8..a1fd4fb 100644
--- a/src/PhpMatcher/JsonContext.php
+++ b/src/PhpMatcher/JsonContext.php
@@ -1,20 +1,15 @@
history = $history;
$this->factory = new MatcherFactory;
- $this->accessor = PropertyAccess::createPropertyAccessor();
}
/** @Then the root should match: */
@@ -39,73 +30,18 @@ final public function root_should_match(PyStringNode $pattern): void
{
$matcher = $this->factory->createMatcher();
- if ($matcher->match($this->getJson(), $pattern->getRaw())) {
+ if ($matcher->match((string) $this->history->getLastResponse()->getBody(), $pattern->getRaw())) {
return;
}
- throw new InvalidArgumentException(
- sprintf(
- 'The json root does not match with the given pattern (error : %s)',
- $matcher->getError()
- )
- );
- }
-
- /** @Then in the json, :path should match: */
- final public function path_should_match(string $path, PyStringNode $pattern): void
- {
- $value = $this->getValue($path);
- $matcher = $this->factory->createMatcher();
-
- $json = json_encode($value);
-
- if ($matcher->match($json, $pattern->getRaw())) {
- return;
- }
-
- throw new InvalidArgumentException(
- sprintf(
- 'The json path "%s" does not match with the given pattern (error : %s)',
- $path,
- $matcher->getError()
- )
- );
- }
-
- /** @Then in the json, :path should not match: */
- final public function path_should_not_match(string $path, PyStringNode $pattern): void
- {
- $value = $this->getValue($path);
- $matcher = $this->factory->createMatcher();
-
- $json = json_encode($value);
-
- if (!$matcher->match($json, $pattern->getRaw())) {
- return;
- }
+ $error = $matcher->getError();
+ assert(is_string($error));
throw new InvalidArgumentException(
sprintf(
- 'The json path "%s" matches with the given pattern (error : %s)',
- $path,
- $matcher->getError()
+ 'The json root does not match with the given pattern (error : %s)',
+ $error
)
);
}
-
- private function getJson(): ?stdClass
- {
- return json_decode((string) $this->history->getLastResponse()->getBody());
- }
-
- private function getValue(string $path)
- {
- $json = $this->getJson();
-
- if (null === $json) {
- throw new InvalidArgumentException('Expected a Json valid content, got none');
- }
-
- return $this->accessor->getValue($json, $path);
- }
}