Skip to content

Commit

Permalink
Add type inference testing for generic classes
Browse files Browse the repository at this point in the history
  • Loading branch information
paulbalandan committed Sep 7, 2024
1 parent b60d25f commit a915a88
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/static-code-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ jobs:
env:
TACHYCARDIA_MONITOR_GA: enabled

- name: Check - Static Analysis
run: composer test:stan
env:
TACHYCARDIA_MONITOR_GA: enabled

- name: Check - PHP-CS-Fixer
run: composer cs:check

Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,25 @@
"test:all": [
"@test:unit",
"@test:auto-review",
"test:stan",
"@test:package"
],
"test:auto-review": "phpunit --group=auto-review --colors=always",
"test:coverage": "@test:unit --coverage-html=build/phpunit/html",
"test:package": "phpunit --group=package-test --colors=always",
"test:stan": "phpunit --group=static-analysis --colors=always",
"test:unit": "phpunit --group=unit-test --colors=always"
},
"scripts-descriptions": {
"cs:check": "Checks for coding style violations",
"cs:fix": "Fixes any coding style violations",
"phpstan:baseline": "Runs PHPStans and dumps resulting errors to baseline",
"phpstan:baseline": "Runs PHPStan and dumps resulting errors to baseline",
"phpstan:check": "Runs PHPStan with identifiers support",
"test:all": "Runs all PHPUnit tests",
"test:auto-review": "Runs the Auto-Review Tests",
"test:coverage": "Runs UnitTests with code coverage",
"test:coverage": "Runs Unit Tests with code coverage",
"test:package": "Runs the Package Tests",
"test:stan": "Runs the Static Analysis Tests",
"test:unit": "Runs the Unit Tests"
}
}
12 changes: 12 additions & 0 deletions phpstan-baseline.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,17 @@
'count' => 1,
'path' => __DIR__ . '/tests/AutoReview/TestCodeTest.php',
];
$ignoreErrors[] = [
// identifier: method.impossibleType
'message' => '#^Call to method Nexus\\\\Option\\\\Some\\<int\\>\\:\\:isNone\\(\\) will always evaluate to false\\.$#',
'count' => 1,
'path' => __DIR__ . '/tests/Option/OptionTest.php',
];
$ignoreErrors[] = [
// identifier: generator.valueType
'message' => '#^Generator expects value type array\\{string, string, list\\<mixed\\>\\}, array given\\.$#',
'count' => 1,
'path' => __DIR__ . '/tests/Option/OptionTypeInferenceTest.php',
];

return ['parameters' => ['ignoreErrors' => $ignoreErrors]];
1 change: 1 addition & 0 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ parameters:
- tools
excludePaths:
analyseAndScan:
- tests/**/data/**
- tests/PHPStan/**/data/**
- tools/vendor/**
bootstrapFiles:
Expand Down
24 changes: 19 additions & 5 deletions src/Nexus/Option/None.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,18 @@ public function isNone(): bool
return true;
}

public function unwrap(): mixed
public function unwrap(): never
{
throw new NoneException();
}

/**
* @template S
*
* @param S $default
*
* @return S
*/
public function unwrapOr(mixed $default): mixed
{
return $default;
Expand All @@ -48,7 +55,7 @@ public function unwrapOrElse(\Closure $default): mixed
return $default();
}

public function map(\Closure $predicate): Option
public function map(\Closure $predicate): self
{
return clone $this;
}
Expand All @@ -63,17 +70,17 @@ public function mapOrElse(\Closure $default, \Closure $predicate): mixed
return $default();
}

public function and(Option $other): Option
public function and(Option $other): self
{
return clone $this;
}

public function andThen(\Closure $predicate): Option
public function andThen(\Closure $predicate): self
{
return clone $this;
}

public function filter(\Closure $predicate): Option
public function filter(\Closure $predicate): self
{
return clone $this;
}
Expand All @@ -88,6 +95,13 @@ public function orElse(\Closure $other): Option
return $other();
}

/**
* @template S
*
* @param Option<S> $other
*
* @return ($other is Some<S> ? Some<S> : self<T>)
*/
public function xor(Option $other): Option
{
return $other->isSome() ? $other : clone $this;
Expand Down
34 changes: 22 additions & 12 deletions src/Nexus/Option/Option.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ interface Option extends \IteratorAggregate
{
/**
* Returns `true` if the option is a **Some** value.
*
* @phpstan-assert-if-true Some<T> $this
* @phpstan-assert-if-true false $this->isNone()
* @phpstan-assert-if-false None $this
* @phpstan-assert-if-false true $this->isNone()
*/
public function isSome(): bool;

Expand All @@ -40,6 +45,11 @@ public function isSomeAnd(\Closure $predicate): bool;

/**
* Returns `true` if the option is a **None** value.
*
* @phpstan-assert-if-true None $this
* @phpstan-assert-if-true false $this->isSome()
* @phpstan-assert-if-false Some<T> $this
* @phpstan-assert-if-false true $this->isSome()
*/
public function isNone(): bool;

Expand Down Expand Up @@ -139,25 +149,25 @@ public function mapOrElse(\Closure $default, \Closure $predicate): mixed;
* passing the result of a function call, it is recommended to use `Option::andThen()`,
* which is lazily evaluated.
*
* @template U
* @template U of Option
*
* @param self<U> $other
* @param U $other
*
* @return self<U>
* @return U
*/
public function and(self $other): self;

/**
* Returns **None** if the option is **None**, otherwise calls `$other` with the wrapped
* value and returns the result.
*
* @template U
* @template U of Option
*
* @param (\Closure(T): self<U>) $predicate
* @param (\Closure(T): U) $predicate
*
* @param-immediately-invoked-callable $predicate
*
* @return self<U>
* @return U
*/
public function andThen(\Closure $predicate): self;

Expand All @@ -182,25 +192,25 @@ public function filter(\Closure $predicate): self;
* passing the result of a function call, it is recommended to use `Option::orElse()`,
* which is lazily evaluated.
*
* @template S
* @template S of Option
*
* @param self<S> $other
* @param S $other
*
* @return self<S>
* @return S
*/
public function or(self $other): self;

/**
* Returns the option if it contains a value, otherwise calls
* `$other` and returns the result.
*
* @template S
* @template S of Option
*
* @param (\Closure(): self<S>) $other
* @param (\Closure(): S) $other
*
* @param-immediately-invoked-callable $other
*
* @return self<S>
* @return S
*/
public function orElse(\Closure $other): self;

Expand Down
52 changes: 49 additions & 3 deletions src/Nexus/Option/Some.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,46 @@ public function unwrap(): mixed
return $this->value;
}

/**
* @return T
*/
public function unwrapOr(mixed $default): mixed
{
return $this->value;
}

/**
* @return T
*/
public function unwrapOrElse(\Closure $default): mixed
{
return $this->value;
}

public function map(\Closure $predicate): Option
/**
* @template U
*
* @param (\Closure(T): U) $predicate
*
* @param-immediately-invoked-callable $predicate
*
* @return self<U>
*/
public function map(\Closure $predicate): self
{
return new self($predicate($this->value));
}

/**
* @template U
*
* @param U $default
* @param (\Closure(T): U) $predicate
*
* @param-immediately-invoked-callable $predicate
*
* @return U
*/
public function mapOr(mixed $default, \Closure $predicate): mixed
{
return $predicate($this->value);
Expand All @@ -87,16 +112,37 @@ public function filter(\Closure $predicate): Option
return $predicate($this->value) ? clone $this : new None();
}

public function or(Option $other): Option
/**
* @template S of Option
*
* @param S $other
*
* @return self<T>
*/
public function or(Option $other): self
{
return clone $this;
}

public function orElse(\Closure $other): Option
/**
* @template S of Option
*
* @param (\Closure(): S) $other
*
* @return self<T>
*/
public function orElse(\Closure $other): self
{
return clone $this;
}

/**
* @template S
*
* @param Option<S> $other
*
* @return ($other is Some<S> ? None : self<T>)
*/
public function xor(Option $other): Option
{
return $other->isSome() ? new None() : clone $this;
Expand Down
4 changes: 2 additions & 2 deletions tests/AutoReview/PhpFilesProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ private static function getTestClasses(): array
if (
! $file->isFile()
|| $file->getExtension() !== 'php'
|| str_contains($file->getPath(), \DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR)
|| str_contains($file->getPath(), \DIRECTORY_SEPARATOR.'data'.\DIRECTORY_SEPARATOR)
|| str_contains($file->getPath(), \DIRECTORY_SEPARATOR.'Fixtures')
|| str_contains($file->getPath(), \DIRECTORY_SEPARATOR.'data')
) {
continue;
}
Expand Down
Loading

0 comments on commit a915a88

Please sign in to comment.