Skip to content
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 typing extra items in unsealed array shapes #250

Merged

Conversation

tscni
Copy link
Contributor

@tscni tscni commented Aug 25, 2024

Part of phpstan/phpstan#8438
Resolves #245

Copy link
Contributor Author

@tscni tscni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the first iteration with some open questions. I would mark it as a draft, but somehow I feel that it'd get less feedback that way.

src/Parser/TypeParser.php Outdated Show resolved Hide resolved
tests/PHPStan/Parser/TypeParserTest.php Outdated Show resolved Hide resolved
src/Ast/Type/ArrayShapeNode.php Outdated Show resolved Hide resolved
Comment on lines 862 to 864
$tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
$extraItemType = $this->parseGeneric(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about adjusting the grammar, but there currently doesn't seem to be a big focus on keeping the grammar maintained. There's e.g. no representation for TOKEN_PHPDOC_EOL at all, ArrayShape is already out of date etc.
I could work on all of that, but again, if there's not much interest in keeping it maintained that doesn't quite seem worthwhile.

Ultimately this leads to the question what the point of the grammar is.
Is it mainly for documentation / easier understanding of the parser logic? Or is there a goal to generate more documentation based on it?
Is the end goal to move to a generated parser?
Should it serve for third party consumers?

I suppose ideally the tests would somehow verify that the parse results match the current grammar.

@@ -22,15 +25,27 @@ class ArrayShapeNode implements TypeNode
/** @var self::KIND_* */
public $kind;

/** @var GenericTypeNode|null */
public $extraItemType;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I'd rather have a different design:

/** @var TypeNode|null */
public $extraKeyType;

/** @var TypeNode|null */
public $extraValueType;

// ...

/**
 * @phpstan-assert-if-false !null $this->extraKeyType
 * @phpstan-assert-if-false !null $this->extraValueType
 */
public function isSealed(): bool
{
}

/**
 * @phpstan-assert-if-true !null $this->extraKeyType
 * @phpstan-assert-if-true !null $this->extraValueType
 */
public function isUnsealed(): bool
{
}

The GenericTypeNode here is too broad, it allows for invalid AST with types count that isn't 2.

Copy link
Contributor Author

@tscni tscni Aug 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mainly went for GenericTypeNode to keep consistency with regular array definitions (e.g. array<string, string, string> works fine on a doc parser level), but I'll happily make it more restrictive.

The assertions on the other hand seem slightly concerning, specifically for unsealed shapes without extra key/value types.

  • Given array{...}
  • We can internally represent that as array{...<array-key, mixed>}
  • And for printing / string conversions output that as array{...} again (also to keep backward compatibility)
  • But this also means that an explicit array{...<array-key, mixed>} would also result in an output of array{...}, unless we add a distinction for "not set"
  • Also, list{...} would then internally be represented as list{...<mixed>} (internally with an irrelvant int key type)

I don't mind doing that, but is the complexity really more suitable in the parser vs. handling it entirely on the type resolution side? (I suppose there's a fine line between the two)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to allow array{...} without any items, we should throw a parse error for that. People should use a general array instead.

Copy link
Contributor Author

@tscni tscni Aug 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's currently allowed. Is that breaking change ok? (see the PR that introduced it #169)

Note that my concerns also apply for e.g. array{foo: string, ...} and list{string, ...}

Copy link
Contributor Author

@tscni tscni Aug 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of note: Psalm allows array{...} & list{...} and, like array{} & list{}, resolves them to array<never, never>: https://psalm.dev/r/7840e76832
Basically meaning that array{...} is still considered sealed by Psalm. (feels like a bug)

In PHPStan they're similarly all resolved to array{} at the moment: https://phpstan.org/r/69df8759-e426-4d5d-a8d3-83d26f973624

Copy link
Member

@ondrejmirtes ondrejmirtes Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I get it. As for the AST in phpdoc-parser, I guess we need to throw away my idea for the methods with @phpstan-assert. We'll keep the public $sealed which will still mean "are there ... in the array shape?". And then we should have a new AST object which will contain both key type nad value type.

So there will be some kind of "ArrayShapeUnsealedTypes|null" property in ArrayShapeNode with $keyType and $valueType. Which will make it impossible to represent impossible states in the object structure.

The only impossible state to represent would be sealed=true and unsealedType !== null but I'm fine to live with that.

As for the PHPStan typesystem representation, we can do the same thing we're doing to array<X> vs. array<array-key, X>.

array{foo: X, ...} would be represented with benevolent union type in the place of unsealed key type, and implicit mixed type in place of the value. So we'd still know what to do in toPhpDocNode().

array<foo: X, ...<array-key, mixed> would still have the benevolent union type, but the mixed would be explicit, so we'd know what to do in toPhpDocNode().

Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only impossible state to represent would be sealed=true and unsealedType !== null but I'm fine to live with that.

There's also the impossible state of $kind === 'list' && $unsealedType->keyType !== null, but that's it.

I've made the necessary adjustments.

@tscni tscni force-pushed the feature/type-unsealed-array-extra-items branch from 088542b to d5fdc37 Compare August 28, 2024 01:59
@tscni tscni force-pushed the feature/type-unsealed-array-extra-items branch from d5fdc37 to a2268bb Compare August 28, 2024 02:02
Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI I pushed a commit with a few tests I was missing :)

use PHPStan\PhpDocParser\Ast\NodeAttributes;
use function sprintf;

class ArrayShapeUnsealedTypeNode implements TypeNode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be just a Node, not TypeNode. It doesn't make sense standalone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. ArrayShapeItemNode shouldn't have been one either then I guess, oh well.
Done.

@tscni
Copy link
Contributor Author

tscni commented Aug 28, 2024

FYI I pushed a commit with a few tests I was missing :)

Thanks :)
I added some more cases for list shapes

@ondrejmirtes ondrejmirtes merged commit 5ceb0e3 into phpstan:1.23.x Aug 29, 2024
43 checks passed
@ondrejmirtes
Copy link
Member

Awesome, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for unsealed shapes with specified other elements' types
2 participants