Skip to content

Add Type::toNativePhpTypeNode() and NativeTypePosition - #6873

Merged
ondrejmirtes merged 1 commit into
2.3.xfrom
to-native-php-type-node
Oct 28, 2026
Merged

ondrejmirtes merged 1 commit into
2.3.xfrom
to-native-php-type-node

Conversation

@ondrejmirtes

@ondrejmirtes ondrejmirtes commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Adds Type::toNativePhpTypeNode(NativeTypePosition $position), the native-declaration counterpart of toPhpDocNode(). It returns Identifier|Name|ComplexType|null (PHPDoc only, so the PHP 7.4 downgrade keeps the interface and implementations compatible). It returns null when the type cannot be written in that position.

The motivation is Rector: its PHPStanStaticTypeMapper keeps about 30 type mappers whose only job is turning PHPStan types back into php-parser type nodes. A prototype that replaces them with this method passes Rector's test suite.

NativeTypePosition

A value object like VerbosityLevel, built from a PHP version:

  • NativeTypePosition::parameter($phpVersion), returnType(), property(), classConstant().
  • It answers feature questions instead of naming the position: isCallableAllowed(), isVoidAllowed(), isStaticAllowed(), isNullableAllowed(), isUnionAllowed(), isIntersectionAllowed(), and so on. A property, for example, does not allow callable.
  • Compound types derive positions for their members: toUnionMember(), toNullableInner(), toIntersectionMember(). IntersectionType only asks isIntersectionAllowed(), so a standalone A&B is emitted from PHP 8.1, while inside a union (DNF) it needs 8.2.

Behaviour

  • Foo|null becomes ?Foo wherever the nullable syntax is allowed; otherwise null goes last in the union.
  • Redundant union members that PHP rejects are dropped: Foo|object, array|iterable, A|(A&B).
  • Accessory types widen to their base (non-empty-string → string, list → array). object&hasMethod(foo) becomes object.
  • Late-resolvable types resolve first. Implicit mixed, resource and error types give null.

TypeToNativePhpTypeNodeTest round-trips every case through ParserNodeTypeToPHPStanType and asserts that the result is a supertype of the original.

Turbo

The shadowed types implement the method natively. The union and intersection logic lives in NativePhpTypeNodeHelper, which both the PHP classes and the extension call. type-family.php compares native and PHP results for 48 types across 20 positions.

@ondrejmirtes
ondrejmirtes force-pushed the to-native-php-type-node branch 2 times, most recently from 7b01a11 to 2e7f04a Compare October 28, 2026 13:03
@ondrejmirtes ondrejmirtes changed the title Add Type::toNativePhpTypeNode() and TypePosition Add Type::toNativePhpTypeNode() and NativeTypePosition Oct 28, 2026
@ondrejmirtes
ondrejmirtes force-pushed the to-native-php-type-node branch 2 times, most recently from f1d95cf to 2fb3e8f Compare October 28, 2026 13:18
return new IdentifierTypeNode('list');
}

/** @return Identifier|Name|ComplexType|null */

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The phpdoc doesn't have to be there. It's implicitly inherited.In case PHPStan complains about too-wide type, you can put a narrower type here.

Go through all similar code and remove extraneous PHPDocs like that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removed the redundant @return PHPDocs from all 40 implementations in 594c472, along with the imports they left unused. PHPStan doesn't need a narrower type anywhere. The only PHPDocs left are on Type::toNativePhpTypeNode() itself and the two static methods of NativePhpTypeNodeHelper, which don't inherit one.

Exports a type as a native PHP type declaration node (Identifier, Name or
ComplexType), or null when the type cannot be expressed in the given
position.

NativeTypePosition is a value object like VerbosityLevel. It carries the PHP
version and answers feature questions (isCallableAllowed(),
isIntersectionAllowed(), isNullableAllowed(), ...) rather than naming the
position. UnionType hands its members a derived position, so an
intersection inside a union is only emitted from PHP 8.2 (DNF) while a
standalone intersection is emitted from PHP 8.1. Foo|null is preferred as
?Foo wherever the nullable syntax is allowed.

The compound halves live in NativePhpTypeNodeHelper so the turbo extension
calls the same code; the other shadowed types implement the method natively.

Amp-Thread-ID: https://ampcode.com/threads/T-01a179f2-995f-7745-8d8c-4c174ba5c0ce
@ondrejmirtes
ondrejmirtes force-pushed the to-native-php-type-node branch from 2fb3e8f to 594c472 Compare October 28, 2026 14:15
@ondrejmirtes
ondrejmirtes merged commit c539adc into 2.3.x Oct 28, 2026
547 checks passed
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.

1 participant