Comments (7)
I found these snippets:
https://psalm.dev/r/307068e874
<?php
abstract class Base
{
/**
* @psalm-assert-if-true A $this
* @psalm-assert-if-false B $this
*/
abstract function isA(): bool;
/**
* @psalm-assert-if-true B $this
* @psalm-assert-if-false A $this
*/
abstract function isB(): bool;
}
class A extends Base
{
function isA(): true { return true; }
function isB(): false { return false; }
}
class B extends Base
{
function isA(): false { return false; }
function isB(): true { return true; }
}
function example(Base $obj): A|B
{
if ($obj->isA()) {
return $obj;
} else {
return $obj;
}
}
function example2(Base $obj): A|B
{
if ($obj->isB()) {
return $obj;
} else {
return $obj;
}
}
Psalm output (using commit ef3b018):
INFO: LessSpecificReturnStatement - 35:16 - The type 'Base' is more general than the declared return type 'A|B' for example
INFO: MoreSpecificReturnType - 30:30 - The declared return type 'A|B' for example is more specific than the inferred return type 'Base'
INFO: LessSpecificReturnStatement - 44:16 - The type 'Base' is more general than the declared return type 'A|B' for example2
INFO: MoreSpecificReturnType - 39:31 - The declared return type 'A|B' for example2 is more specific than the inferred return type 'Base'
https://psalm.dev/r/7a76f6d64e
<?php
/**
* @psalm-assert-if-true 'A' $value
* @psalm-assert-if-false 'B' $value
*/
function isA(string $value): bool
{
return $value === 'A';
}
/**
* @psalm-assert-if-true 'B' $value
* @psalm-assert-if-false 'A' $value
*/
function isB(string $value): bool
{
return $value === 'B';
}
/** @return 'A'|'B' */
function example(string $value): string
{
if (isA($value)) {
return $value;
} else {
return $value;
}
}
/** @return 'A'|'B' */
function example2(string $value): string
{
if (isB($value)) {
return $value;
} else {
return $value;
}
}
Psalm output (using commit ef3b018):
INFO: LessSpecificReturnStatement - 27:16 - The type 'string' is more general than the declared return type ''A'|'B'' for example
INFO: MoreSpecificReturnType - 21:13 - The declared return type ''A'|'B'' for example is more specific than the inferred return type 'string'
INFO: LessSpecificReturnStatement - 37:16 - The type 'string' is more general than the declared return type ''A'|'B'' for example2
INFO: MoreSpecificReturnType - 31:13 - The declared return type ''A'|'B'' for example2 is more specific than the inferred return type 'string'
from psalm.
This is not a bug.
If you return false it can be any class that extends Base
not just B
.
When adding a class C you can see the problem: https://psalm.dev/r/eb47b05687
I guess there should be an error added to not allow true + false assertions if BOTH of them use a class, so this is reported in line 6/7 and 12/13 already.
from psalm.
I found these snippets:
https://psalm.dev/r/eb47b05687
<?php
abstract class Base
{
/**
* @psalm-assert-if-true A $this
* @psalm-assert-if-false B $this
*/
abstract function isA(): bool;
/**
* @psalm-assert-if-true B $this
* @psalm-assert-if-false A $this
*/
abstract function isB(): bool;
}
class A extends Base
{
function isA(): true { return true; }
function isB(): false { return false; }
}
class B extends Base
{
function isA(): false { return false; }
function isB(): true { return true; }
}
class C extends Base {
function isA(): false { return false; }
function isB(): false { return false; }
}
function example(Base $obj): A|B
{
if ($obj->isA()) {
return $obj;
} else {
return $obj;
}
}
function example2(Base $obj): A|B
{
if ($obj->isB()) {
return $obj;
} else {
return $obj;
}
}
Psalm output (using commit ef3b018):
INFO: LessSpecificReturnStatement - 40:16 - The type 'Base' is more general than the declared return type 'A|B' for example
INFO: MoreSpecificReturnType - 35:30 - The declared return type 'A|B' for example is more specific than the inferred return type 'Base'
INFO: LessSpecificReturnStatement - 49:16 - The type 'Base' is more general than the declared return type 'A|B' for example2
INFO: MoreSpecificReturnType - 44:31 - The declared return type 'A|B' for example2 is more specific than the inferred return type 'Base'
from psalm.
These are just examples - I think it's up to the method to implement its own assertions correctly.
I can use final methods: https://psalm.dev/r/41b44c332d
Or assertions: https://psalm.dev/r/342dd21302
It doesn't work with enums either (enums are always final): https://psalm.dev/r/dd4afda62e (this works in PHPStan)
from psalm.
I found these snippets:
https://psalm.dev/r/41b44c332d
<?php
abstract class Base
{
/**
* @psalm-assert-if-true A $this
* @psalm-assert-if-false B $this
*/
final function isA(): bool
{
return match (true) {
$this instanceof A => true,
$this instanceof B => false,
default => throw new \LogicException(),
};
}
/**
* @psalm-assert-if-true B $this
* @psalm-assert-if-false A $this
*/
final function isB(): bool
{
return match (true) {
$this instanceof A => false,
$this instanceof B => true,
default => throw new \LogicException(),
};
}
}
class A extends Base
{
}
class B extends Base
{
}
class C extends Base {
function isA(): false { return false; }
function isB(): false { return false; }
}
function example(Base $obj): A|B
{
if ($obj->isA()) {
return $obj;
} else {
return $obj;
}
}
function example2(Base $obj): A|B
{
if ($obj->isB()) {
return $obj;
} else {
return $obj;
}
}
Psalm output (using commit ef3b018):
INFO: LessSpecificReturnStatement - 50:16 - The type 'Base' is more general than the declared return type 'A|B' for example
INFO: MoreSpecificReturnType - 45:30 - The declared return type 'A|B' for example is more specific than the inferred return type 'Base'
INFO: LessSpecificReturnStatement - 59:16 - The type 'Base' is more general than the declared return type 'A|B' for example2
INFO: MoreSpecificReturnType - 54:31 - The declared return type 'A|B' for example2 is more specific than the inferred return type 'Base'
ERROR: MethodSignatureMismatch - 41:5 - Method Base::isA is declared final and cannot be overridden
ERROR: MethodSignatureMismatch - 42:5 - Method Base::isB is declared final and cannot be overridden
https://psalm.dev/r/342dd21302
<?php
/**
* @psalm-assert-if-true 'A' $value
* @psalm-assert-if-false 'B' $value
*/
function isA(string $value): bool
{
assert($value === 'A' || $value === 'B');
return $value === 'A';
}
/**
* @psalm-assert-if-true 'B' $value
* @psalm-assert-if-false 'A' $value
*/
function isB(string $value): bool
{
assert($value === 'A' || $value === 'B');
return $value === 'B';
}
/** @return 'A'|'B' */
function example(string $value): string
{
if (isA($value)) {
return $value;
} else {
return $value;
}
}
/** @return 'A'|'B' */
function example2(string $value): string
{
if (isB($value)) {
return $value;
} else {
return $value;
}
}
Psalm output (using commit ef3b018):
INFO: LessSpecificReturnStatement - 31:16 - The type 'string' is more general than the declared return type ''A'|'B'' for example
INFO: MoreSpecificReturnType - 25:13 - The declared return type ''A'|'B'' for example is more specific than the inferred return type 'string'
INFO: LessSpecificReturnStatement - 41:16 - The type 'string' is more general than the declared return type ''A'|'B'' for example2
INFO: MoreSpecificReturnType - 35:13 - The declared return type ''A'|'B'' for example2 is more specific than the inferred return type 'string'
https://psalm.dev/r/dd4afda62e
<?php
enum Example
{
case A;
case B;
/**
* @psalm-assert-if-true Example::A $this
* @psalm-assert-if-false Example::B $this
*/
function isA(): bool
{
return $this === self::A;
}
/**
* @psalm-assert-if-true Example::B $this
* @psalm-assert-if-false Example::A $this
*/
function isB(): bool
{
return $this === self::B;
}
}
function example(Example $obj): void
{
if ($obj->isA()) {
/** @psalm-trace $obj */;
} else {
/** @psalm-trace $obj */;
}
}
function example2(Example $obj): void
{
if ($obj->isB()) {
/** @psalm-trace $obj */;
} else {
/** @psalm-trace $obj */;
}
}
Psalm output (using commit ef3b018):
ERROR: TypeDoesNotContainType - 29:9 - Type enum(Example::A) for enum(Example::A) is always !enum(Example::B)
INFO: Trace - 30:33 - $obj: never
INFO: Trace - 32:33 - $obj: enum(Example::B)
ERROR: TypeDoesNotContainType - 38:9 - Type enum(Example::B) for enum(Example::B) is always !enum(Example::A)
INFO: Trace - 39:33 - $obj: never
INFO: Trace - 41:33 - $obj: enum(Example::A)
from psalm.
It seems to work when combined with @psalm-this-out A|B
:
https://psalm.dev/r/3efc302338
I'm not sure if this workaround should be necessary however.
Edit: It also seems to break eventually anyway: https://psalm.dev/r/9ea25aa6e3
from psalm.
I found these snippets:
https://psalm.dev/r/3efc302338
<?php
abstract class Base
{
/**
* @psalm-assert-if-true A $this
* @psalm-assert-if-false B $this
* @psalm-this-out A|B
*/
final function isA(): bool
{
return match (true) {
$this instanceof A => true,
$this instanceof B => false,
default => throw new \LogicException(),
};
}
/**
* @psalm-assert-if-true B $this
* @psalm-assert-if-false A $this
* @psalm-this-out A|B
*/
final function isB(): bool
{
return match (true) {
$this instanceof A => false,
$this instanceof B => true,
default => throw new \LogicException(),
};
}
}
class A extends Base
{
}
class B extends Base
{
}
function example(Base $obj): A|B
{
if ($obj->isA()) {
return $obj;
} else {
return $obj;
}
}
function example2(Base $obj): A|B
{
if ($obj->isB()) {
return $obj;
} else {
return $obj;
}
}
Psalm output (using commit ef3b018):
No issues!
https://psalm.dev/r/9ea25aa6e3
<?php
/**
* @template T
*/
abstract class Maybe
{
/**
* @psalm-assert-if-true Some<T> $this
* @psalm-assert-if-false None $this
* @psalm-this-out Some<T>|None
*/
final function isSome(): bool
{
return match (true) {
$this instanceof Some => true,
$this instanceof None => false,
default => throw new \LogicException(),
};
}
/**
* @psalm-assert-if-true None $this
* @psalm-assert-if-false Some<T> $this
* @psalm-this-out Some<T>|None
*/
final function isNone(): bool
{
return match (true) {
$this instanceof Some => false,
$this instanceof None => true,
default => throw new \LogicException(),
};
}
}
/**
* @template T
* @extends Maybe<T>
*/
class Some extends Maybe
{
}
/**
* @extends Maybe<never>
*/
class None extends Maybe
{
}
/**
* @param Maybe<string> $maybe
* @return Some<string>|None
*/
function example(Maybe $maybe): Some|None
{
if ($maybe->isSome()) {
return $maybe;
} else {
return $maybe;
}
}
/**
* @param Maybe<string> $maybe
* @return Some<string>|None
*/
function example2(Maybe $maybe): Some|None
{
if ($maybe->isNone()) {
return $maybe;
} else {
return $maybe;
}
}
/**
* @param Maybe<string> $maybe
* @return Some<string>|None
*/
function example3(Maybe $maybe): Some|None
{
if ($maybe->isSome()) {
/** @psalm-trace $maybe */;
} else {
/** @psalm-trace $maybe */;
}
if ($maybe->isNone()) {
/** @psalm-trace $maybe */;
} else {
/** @psalm-trace $maybe */; // Some<never>, but should be Some<string>
}
return $maybe;
}
Psalm output (using commit ef3b018):
INFO: Trace - 85:35 - $maybe: Some<string>
INFO: Trace - 87:35 - $maybe: None
INFO: Trace - 91:35 - $maybe: None
INFO: Trace - 93:35 - $maybe: Some<never>
from psalm.
Related Issues (20)
- InvalidArgument error introduced in 5.24 HOT 7
- ArrayAccess with nullable value and ?? gives PossiblyNullReference HOT 3
- FalsePositive ImpureVariable in constructor HOT 1
- Support declaring known type variadic named arguments HOT 1
- Add error for negated file ops in loop when clearstatcache isn't called HOT 1
- Enum::from and Enum::tryFrom doesn't work with templates HOT 1
- psalm-if-this-is crashes with enums HOT 2
- enums: can't assign a value to value-of<Enum> HOT 1
- Type bounds for generic parameters HOT 1
- MissingOverrideAttribute false positive with traits used by child classes HOT 3
- Psalm's inference of array keys as an `int<min, max>` range clashes with invariance of templated collection types HOT 1
- Array shapes don't recognize nullable members HOT 2
- Regression in 5.5.0 when using more than one template parameter? HOT 3
- MissingClassConstType is not documented
- ensureArrayString/Int/Offset seems to be unsound for non-literal array offsets HOT 7
- properties-of<Class> not compatible with array type HOT 3
- psalm not understanding properties-of<Class> assignment to a ParentClass property, if properties-of<ParentClass> is used HOT 1
- Unbound template type is not narrowed correctly HOT 2
- The "Generator" class is reserved for internal use and cannot be manually instantiated HOT 1
- Psalm adds float to inferred return type when two integer numbers are added together HOT 4
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from psalm.