-
Notifications
You must be signed in to change notification settings - Fork 679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support type annotations for class consts (fixes #942). #7123
Support type annotations for class consts (fixes #942). #7123
Conversation
|
||
class UnresolvableConstant extends CodeIssue | ||
{ | ||
public const ERROR_LEVEL = -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be set to something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe -1 means that the error will be emitted no matter what level the user is on. It's usually reserved to obvious errors or issue behind a specific flag.
I'm not sure I see yet in what scenario this issue could be emitted, but you have to decide a level based on severity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-1 denotes insuppressible issue, -2 - issues controlled by a flag (like --find-dead-code
).
So -1 should be reserved for certain fatal errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be emitted when key-of
or value-of
is used on a constant that can't be resolved, which I think probably should always be emitted, as it's indicative of a typo or other issue with the type annotation, and it's an annotation that's specifically for static analysis. I never use any of the less strict error levels though, so I'm not confident I should be the one deciding.
tests/ConstantTest.php
Outdated
', | ||
'error_message' => 'UnresolvableConstant', | ||
], | ||
'SKIPPED-keyofSelfConstDoesntImplyKeyofStaticConst' => [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to do, but psalm isn't catching this either: https://psalm.dev/r/4efbbbda75
I thought there was an option to be strict about array keys somewhere, but I'm not seeing it at the moment, and I don't know how I would enable that for the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you might need those two:
<xs:attribute name="ensureArrayIntOffsetsExist" type="xs:boolean" default="false" />
<xs:attribute name="ensureArrayStringOffsetsExist" type="xs:boolean" default="false" />
To enable it you would have to extract the case into a full-fledged test method, like this:
psalm/tests/ArrayAccessTest.php
Lines 17 to 36 in 3cce691
public function testEnsureArrayOffsetsExist(): void | |
{ | |
$this->expectException(CodeException::class); | |
$this->expectExceptionMessage('PossiblyUndefinedStringArrayOffset'); | |
Config::getInstance()->ensure_array_string_offsets_exist = true; | |
$this->addFile( | |
'somefile.php', | |
'<?php | |
function takesString(string $s): void {} | |
/** @param array<string, string> $arr */ | |
function takesArrayIteratorOfString(array $arr): void { | |
echo $arr["hello"]; | |
}' | |
); | |
$this->analyzeFile('somefile.php', new Context()); | |
} |
I just tested #5743 and it seems to work, but if you don't add an argument it reports "Too few arguments for - expecting 1 but saw 0". Seems like the Edit: Actually I guess that's sort of expected, since the type should be |
class Foo | ||
{ | ||
public const BAR = ["bar"]; | ||
|
||
/** | ||
* @return value-of<static::BAR> | ||
*/ | ||
public function bar(): string | ||
{ | ||
return static::BAR[0]; | ||
} | ||
} | ||
', | ||
'error_message' => 'UnresolvableConstant', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this really be UnresolvableConstant
? I think I'd find it confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this really be
UnresolvableConstant
? I think I'd find it confusing.
Yeah, perhaps a bit confusing, but I made a note of it in the UnresolvableConstant
documentation. I didn't really want to have another issue for IllegalConstant
or something like that. I'm happy to change the name if you can come up with something that intuitively encompasses both usages, or would you like to have a separate issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe UnresolvableLiteralConstant
?
tests/ConstantTest.php
Outdated
', | ||
'error_message' => 'UnresolvableConstant', | ||
], | ||
'SKIPPED-keyofSelfConstDoesntImplyKeyofStaticConst' => [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you might need those two:
<xs:attribute name="ensureArrayIntOffsetsExist" type="xs:boolean" default="false" />
<xs:attribute name="ensureArrayStringOffsetsExist" type="xs:boolean" default="false" />
To enable it you would have to extract the case into a full-fledged test method, like this:
psalm/tests/ArrayAccessTest.php
Lines 17 to 36 in 3cce691
public function testEnsureArrayOffsetsExist(): void | |
{ | |
$this->expectException(CodeException::class); | |
$this->expectExceptionMessage('PossiblyUndefinedStringArrayOffset'); | |
Config::getInstance()->ensure_array_string_offsets_exist = true; | |
$this->addFile( | |
'somefile.php', | |
'<?php | |
function takesString(string $s): void {} | |
/** @param array<string, string> $arr */ | |
function takesArrayIteratorOfString(array $arr): void { | |
echo $arr["hello"]; | |
}' | |
); | |
$this->analyzeFile('somefile.php', new Context()); | |
} |
So here's something I'm not sure of: should we default to the annotated type or the inferred type?
Unfortunately, I could see places where one might want the behavior of 2 without the behavior of 1. Should this be configurable? Should we just go with 1 and say if you annotate it, that's what psalm is going to use? Contrived illustration of where 2 without 1 may be useful: https://psalm.dev/r/8a9abce968 Edit: I suppose one possible solution is to say if you use |
I found these snippets: https://psalm.dev/r/8a9abce968<?php
class Foo
{
/** @var string */
public const BAR = "bar";
/**
* @return "bar"
*/
public function bar(): string
{
return self::BAR;
}
public function childBar(): string
{
return static::BAR;
}
}
|
Is there actually a difference between 1 and 2 though? |
In the example I gave it should work fine with 2, but with 1 it will have an issue for |
#5743 is broken again after I fixed a mistake. I meant to use the inferred type except when using late static binding, rather than use the annotated type except for with My preference is to use the inferred type wherever possible and explicitly opt out of that, because it allows a lot more static analysis, but right now there's no way to opt out and say you want the annotated type to be used for a particular constant. I'm tempted to say that the fix for #5743 should actually be to allow interpreting |
* @param array|scalar|null $value | ||
* | ||
*/ | ||
public static function getLiteralTypeFromValue($value): Type\Union |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this function already exists somewhere and I missed it. Should this go somewhere else? Maybe as Type::getLiteral
?
I'd say we should follow the principle of least surprise and just use the declared type, and also flag cases where the constant value contradicts the declared type. As far as inheritance is concerned, constant types should be covariant. Do you see any problems with this approach? |
Well, It also causes annoyances like this: https://psalm.dev/r/cde953e232
I meant to do that and completely forgot about it. |
I found these snippets: https://psalm.dev/r/cde953e232<?php
class Foo {
/**
* I don't care about child classes, this annotation just exists
* to make sure all elements match the type.
*
* @var array<string, array{foo: string, bar: int}>
*/
public const CONFIG = [
"a" => ["foo" => "foo", "bar" => 1],
];
/**
* @param key-of<self::CONFIG> $key
*
* @return array{foo: string, bar: int}
*/
public function getConfig(string $key): array
{
// Should cause PossiblyUndefinedStringArrayOffset because the
// annotated type is used, even though I don't want it to be
return self::CONFIG[$key];
}
}
|
e2e88ec
to
0f899ac
Compare
@AndrolGenhald what is the status of this? Are you waiting for another PR, for a review or do you still need to work on it? |
1d3dfe9
to
21edfcc
Compare
I think the only thing here that might be controversial is what I mentioned here, I have it defaulting to using the inferred type whenever possible, so the I can see where some people might want to override the constant type, but that would prevent analysis like this where you want to constrain the types a child can override the constant with, but you still want to use the inferred type for other analysis (returning a literal in that example). I think using the inferred type wherever possible is more useful, and it more closely matches the way psalm handles constants now when it ignores the |
I found these snippets: https://psalm.dev/r/8a9abce968<?php
class Foo
{
/** @var string */
public const BAR = "bar";
/**
* @return "bar"
*/
public function bar(): string
{
return self::BAR;
}
public function childBar(): string
{
return static::BAR;
}
}
|
So, just to be sure, I see 3 possible types for a class const.
For me, const should be invariant, like properties, so the type of the parent const must be the same as the child. I think this is where @var will be useful to define a standard type among all childs/parent (for example, if you have a parent that defined "hello" and a child "hello2", both should be @var'ed to 'string' So Psalm just need to make sure that the type from the @var is consistent with the content of the const. And then apply invariant checks. Do I make sense? |
@orklah We discussed this before at #5589, do you want to move the discussion over there? I think it makes more sense for const types to be covariant rather than invariant, see my last comment there with the examples. |
Ok, now that we settled we're indeed covariant, it makes things easier I think. As long as the phpdoc type is consistent with the content of the constant, the phpdoc is allowed. Then the phpdoc is always the default because it can be more precise |
Ok, fine by me then :) Thanks for taking the time to explain :p |
* @throws InvalidArgumentException | ||
* | ||
*/ | ||
public static function getLiteralTypeFromValue($value, bool $sealed_array = true): Type\Union |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe getLiteralTypeFromScalarValue could be reused or refactored or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I overlooked that one, I'll take a look at it in a bit. One thing I'm not too happy about is that allowing array
here could allow a non-scalar to be passed, but I don't think we have a way to type "possibly deeply nested array with only scalars allowed".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. I'm half tempted to try adding something like TNestedArray
where nested-array<scalar>
would be like array<scalar|array<scalar|array...>>
, but I'm not sure how useful it would be in practice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be a hell to add I think. It means you'll have to implement the logic to compare any array to the nested array.
That means:
- can any array be equal to the nested one
- can any array be contained in the nested and the other way around
- nested assertions probably
etc...
No problem, convincing someone is a really good sanity check. |
21edfcc
to
1f1f1c5
Compare
Thanks! |
That's weird, PHPUnit test and Psalm itself started to output the same kind of error after I merged. They weren't on CI. Could you take a look at it to see if they can be fixed? |
@orklah I assume you're referring to |
Mmh, it may actually be one of my PR when I removed positive-int. I'll try to take a look at it |
I think I'm closing in on it, I'm trying to figure out why it's getting the wrong type set in ClassLikeNodeScanner. |
@orklah TypeCombiner.php:1195:
Should be 0 there, 0 is non-negative. It's not immediately clear to me how it worked before, the literal |
Yeah, I just saw that too. It was new TPositiveInt before my change, it just made it appear. I'll fix that |
Ah, I see, there's a |
Thanks btw! You're pretty efficient at finding bugs in Psalm! |
No problem, I enjoy debugging 🙂 |
I found these snippets: https://psalm.dev/r/3531bf3469<?php
class Foo
{
/** @var non-empty-list<int> */
public const BAZ = [1];
/** @return value-of<static::BAZ> */
public static function foobar(int $int): int
{
return static::BAZ[0];
}
}
|
No description provided.