Skip to content

infer non-empty-list/array after isset($arr[$i])#4441

Merged
staabm merged 13 commits intophpstan:2.1.xfrom
staabm:narr
Mar 23, 2026
Merged

infer non-empty-list/array after isset($arr[$i])#4441
staabm merged 13 commits intophpstan:2.1.xfrom
staabm:narr

Conversation

@staabm
Copy link
Contributor

@staabm staabm commented Oct 15, 2025

analog #4440 but for isset($arr[$i])

closes phpstan/phpstan#13674
closes phpstan/phpstan#12482
closes phpstan/phpstan#13675

@staabm staabm marked this pull request as ready for review March 21, 2026 15:47
@staabm staabm requested a review from VincentLanglet March 21, 2026 15:47
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

Copy link
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

The getNative hack is definetly non acceptable since the bug will occurs if you add native typehint 'array' to the Bug7511.

namespace Bug7511;

use function PHPStan\Testing\assertType;

interface PositionEntityInterface {
	public function getPosition(): int;
}
interface TgEntityInterface {}

abstract class HelloWorld
{
	/**
	 * @phpstan-template T of PositionEntityInterface&TgEntityInterface
	 *
	 * @param array<T> $tgs
	 *
	 * @return array<T>
	 *
	 * @throws \Exception
	 */
	public function computeForFrontByPosition(array $tgs)
	{
		/** @phpstan-var array<T> $res */
		$res = [];

		assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition(), parameter)', $res[1]);

		foreach ($tgs as $tgItem) {
			$position = $tgItem->getPosition();

			if (!isset($res[$position])) {
				assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition(), argument)', $tgItem);
				$res[$position] = $tgItem;
			} else {
				assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition(), argument)', $tgItem);
				assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition(), parameter)', $res[$position]);
				$tgItemToKeep   = $this->compare($tgItem, $res[$position]);
				assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition(), parameter)', $tgItemToKeep);
				$res[$position] = $tgItemToKeep;
			}
		}
		ksort($res);

		assertType('array<T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition(), parameter)>', $res);

		return $res;
	}

	/**
	 * @phpstan-template T of PositionEntityInterface&TgEntityInterface
	 *
	 * @param array<T> $tgs
	 *
	 * @return array<T>
	 *
	 * @throws \Exception
	 */
	public function computeForFrontByPosition2(array $tgs)
	{
		/** @phpstan-var array<T> $res */
		$res = [];

		assertType('array<T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition2(), parameter)>', $res);

		foreach ($tgs as $tgItem) {
			$position = $tgItem->getPosition();

			assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition2(), parameter)', $res[$position]);
			if (isset($res[$position])) {
				assertType('T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition2(), parameter)', $res[$position]);
			}
		}
		assertType('array<T of Bug7511\PositionEntityInterface&Bug7511\TgEntityInterface (method Bug7511\HelloWorld::computeForFrontByPosition2(), parameter)>', $res);

		return $res;
	}

	/**
	 * @phpstan-template S of TgEntityInterface
	 * @phpstan-param S $nextTg
	 * @phpstan-param S $currentTg
	 * @phpstan-return S
	 */
	abstract protected function compare(TgEntityInterface $nextTg, TgEntityInterface $currentTg): TgEntityInterface;
}

@VincentLanglet
Copy link
Contributor

@staabm with some debugging, I end up with ArrayDimFetchHandler calling IntersectionType::getOffsetValueType which does TypeCombinator::intersect(TemplateIntersectionType, MixedType) which gives a IntersectionType instead of a TemplateIntersectionType one.

The bug is created because Array<TemplateIntersectionType> is now specified to Array<TemplateIntersectionType>&NonEmptyArray (which is an IntersectionType) after your PR.

We could eventually start by fix/improve TypeCombinator::intersect with TemplateIntersectionType.

I hope it helps you

@staabm
Copy link
Contributor Author

staabm commented Mar 23, 2026

yeah, I agree. its a similar observeration that what we found out in #4441 (comment)

maybe we need something like #3535

@staabm
Copy link
Contributor Author

staabm commented Mar 23, 2026

underlying generics problem was separated into phpstan/phpstan#14348

@staabm staabm merged commit bebb16f into phpstan:2.1.x Mar 23, 2026
649 of 652 checks passed
@staabm staabm deleted the narr branch March 23, 2026 12:38
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.

infer non-empty-list after isset($list[$i])

3 participants