Skip to content

Conversation

staabm
Copy link
Contributor

@staabm staabm commented Sep 26, 2025

@staabm
Copy link
Contributor Author

staabm commented Sep 27, 2025

reduced reproducer of the FileTypeMapper false-positive error we get right now:

<?php declare(strict_types = 1);

namespace PHPStan\Type;

use function is_callable;

final class NameScope {}

final class FileTypeMapper
{

	/** @var (true|callable(): NameScope|NameScope)[][] */
	private array $inProcess = [];

	public function getNameScope(
		string $fileName,
		?string $className,
		?string $traitName,
		?string $functionName,
	): NameScope
	{
		$nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName);
		if (!isset($this->inProcess[$fileName][$nameScopeKey])) { // wrong $fileName due to traits
			throw new \RuntimeException();
		}

		if ($this->inProcess[$fileName][$nameScopeKey] === true) { // PHPDoc has cyclic dependency
			throw new \RuntimeException();
		}

		if (is_callable($this->inProcess[$fileName][$nameScopeKey])) {
			$resolveCallback = $this->inProcess[$fileName][$nameScopeKey];
			$this->inProcess[$fileName][$nameScopeKey] = true;
			$this->inProcess[$fileName][$nameScopeKey] = $resolveCallback();
		}

		return $this->inProcess[$fileName][$nameScopeKey];
	}

	private function getNameScopeKey(
		?string $file,
		?string $class,
		?string $trait,
		?string $function,
	): string
	{
		return '';
	}

}

leads to

------ --------------------------------------------------------------------------------------------------------------------------------------------- 
  Line   test7.php                                                                                                                                    
 ------ --------------------------------------------------------------------------------------------------------------------------------------------- 
  37     Method PHPStan\Type\FileTypeMapper::getNameScope() should return PHPStan\Type\NameScope but returns (callable)|PHPStan\Type\NameScope|true.  
         🪪  return.type                                                                                                                              
         at test7.php:37                                                                                                                              
 ------ --------------------------------------------------------------------------------------------------------------------------------------------- 

needs investigation fixed in 72177cf

Comment on lines +4314 to +4317
&& !$expr->dim instanceof Expr\PreInc
&& !$expr->dim instanceof Expr\PreDec
&& !$expr->dim instanceof Expr\PostDec
&& !$expr->dim instanceof Expr\PostInc
Copy link
Contributor Author

@staabm staabm Sep 27, 2025

Choose a reason for hiding this comment

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

required change to not regress existing tests like

<?php

$anotherIndex = 0;
$postIncArray = [];
$postIncArray[$anotherIndex++] = $anotherIndex++;
\PHPStan\Testing\assertType('array{1}', $postIncArray);

Comment on lines 374 to 388
if ($this->itemType->isConstantArray()->yes() && $valueType->isConstantArray()->yes()) {
$constArrays = $valueType->getConstantArrays();
$newItemType = $this->itemType;
foreach($constArrays as $constArray) {
foreach($constArray->getKeyTypes() as $keyType) {
$newItemType = $newItemType->setExistingOffsetValueType($keyType, $constArray->getOffsetValueType($keyType));
}
}

if ($newItemType !== $this->itemType) {
return new self(
$this->keyType,
$newItemType
);
}
}
Copy link
Contributor Author

@staabm staabm Sep 27, 2025

Choose a reason for hiding this comment

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

required to prevent regression in

@ -0,0 +1,22 @@
<?php

namespace AssignNestedArrays;

use function PHPStan\debugScope;
use function PHPStan\Testing\assertType;

class Foo
{
	public function doBar(int $i, int $j)
	{
		$array = [];

		$array[$i][$j]['bar'] = 1;
		debugScope();
		$array[$i][$j]['baz'] = 2;
		debugScope();

		assertType('non-empty-array<int, non-empty-array<int, array{bar: 1, baz: 2}>>', $array);
	}

}

@staabm
Copy link
Contributor Author

staabm commented Sep 27, 2025

ok - I think after all required PRs (see description) have been merged, this one will be ready to land

@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

}

if ($scope->hasExpressionType($arrayDimFetch)->yes()) { // keep list for $list[$index] assignments
if (!$arrayDimFetch->dim instanceof BinaryOp\Plus) {
Copy link
Contributor Author

@staabm staabm Sep 28, 2025

Choose a reason for hiding this comment

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

@ondrejmirtes why is the issue related to Plus? there is no + contained in the related code example?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ohh.. the Plus is related to the list-inference logic below.

so it was a bug lingering for longer already :)

@staabm
Copy link
Contributor Author

staabm commented Sep 28, 2025

looking at issue bot, the last fix also fixes more stuff.. adding regression tests now..

/**
* @param list<ArrayDimFetch> $dimFetchStack
* @param list<Type|null> $offsetTypes
* @param list<array{Expr, Type}> $additionalExpressions
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of by-ref parameters. I'd rather if the method returned array{Type, list<array{Expr, Type}>} 😊

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@ondrejmirtes ondrejmirtes merged commit ff2ce20 into phpstan:2.1.x Sep 28, 2025
542 of 549 checks passed
@ondrejmirtes
Copy link
Member

Awesome, thank you!

@staabm staabm deleted the bug13538 branch September 28, 2025 10:42
@ondrejmirtes
Copy link
Member

This is really nice PR and very solid work, but I found a regression: https://phpstan.org/r/16197474-84cb-43ad-b983-ad78371fba02

My guess is that the bug will be somewhere in MutatingScope::generalizeWith().

@ondrejmirtes
Copy link
Member

And another one: https://phpstan.org/r/80fa95b4-c7a0-4591-937d-efbf39561d95 (the nullCoalesce.offset ones)

I guess it's not really easy to reason about that. After the for loop iteration, we can be sure that $stats[$author] contains all three offsets commits/files/insertions, but when evaluating:

$stats[$author]['files'] = ($stats[$author]['files'] ?? 0) + $files;

We don't know whether $stats[$author] already has 'files' offset from the previous iteration, or if it's a new author that only just has 'commits' offset.

@staabm
Copy link
Contributor Author

staabm commented Sep 29, 2025

My guess is that the bug will be somewhere in MutatingScope::generalizeWith().

my gut feeling is we miss to generalize key/value types when generalizeType(ConstArray, GeneralArray) is compared
(so we miss to recurse into key/value types in this case)

@ondrejmirtes
Copy link
Member

I have a different theory now and possible solution - it's a problem when merging scopes.

The $array variable type will be merged because it's in both scopes but then there's also $array[$k] which will only be in the inner scope but not the outer scope so it will remain with the same type in the final scope after merging and certainty Maybe.

So I think when we're merging scopes and the type of $array changes, we need to throw away all other expressions that contain $array and not merge them.

I think somewhere in MutatingScope we do or used to do in the past was to sort expressions by length so that the shorter ones are evaluated first. It might be useful in merging too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment