Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
72b0c53
Guard old-world type resolution entry points behind NewWorld::disable…
ondrejmirtes Jun 9, 2026
cdcb73f
Deliver ExpressionResult (with type callbacks) to FiberScope and engine
ondrejmirtes Jun 9, 2026
2bea240
Add NEW_WORLD.md design document
ondrejmirtes Jun 10, 2026
d6e3cb1
Migrate ScalarHandler, AssignHandler and FuncCallHandler to the new w…
ondrejmirtes Jun 10, 2026
699b3c9
Apply SpecifiedTypes the new-world way and narrow in if/else via Expr…
ondrejmirtes Jun 10, 2026
0f3c002
Migrate Array, BinaryOp and inc/dec handlers; fix premature pending-f…
ondrejmirtes Jun 10, 2026
d0d0733
Resolve FiberScope types at the expression's evaluation point, narrow…
ondrejmirtes Jun 10, 2026
cd20b02
Bridge per-scalar holder narrowing through old-world equality and kee…
ondrejmirtes Jun 10, 2026
45532d4
Price function-call extension reads at the call point
ondrejmirtes Jun 10, 2026
6304230
Flush pending fibers before building the class aggregate nodes
ondrejmirtes Jun 10, 2026
c56069a
Collect aggregate data before forwarding nodes to rules
ondrejmirtes Jun 10, 2026
1f3db75
Promote the call-point adapter for native-type asks in the specify pa…
ondrejmirtes Jun 10, 2026
13ce310
Build Ternary/Match conditional-expression holders in mixed mode
ondrejmirtes Jun 10, 2026
186cccd
Use Yes-certainty holders only as narrowing originals in applySpecifi…
ondrejmirtes Jun 10, 2026
7a28e95
Record the whole-suite burn-down leg in the status log
ondrejmirtes Jun 10, 2026
a4c5696
Type-free default narrowing and acyclic result graphs
ondrejmirtes Jun 10, 2026
7369daf
Migrate PropertyFetchHandler and NullsafePropertyFetchHandler to the …
ondrejmirtes Jun 10, 2026
7e43926
Migrate NullsafeMethodCallHandler to the new world
ondrejmirtes Jun 10, 2026
39ff793
Migrate BooleanAndHandler and BooleanOrHandler to the new world
ondrejmirtes Jun 10, 2026
a9beb4f
Migrate TernaryHandler and BooleanNotHandler to the new world
ondrejmirtes Jun 10, 2026
11ceac8
Replace statement-level Scope::getType() asks in NodeScopeResolver wi…
ondrejmirtes Jun 10, 2026
7e27e5c
Migrate ConstFetchHandler to the new world
ondrejmirtes Jun 10, 2026
b41e139
Migrate UnaryMinusHandler to the new world
ondrejmirtes Jun 10, 2026
a941cac
Migrate UnaryPlusHandler to the new world
ondrejmirtes Jun 10, 2026
ead55f1
Migrate BitwiseNotHandler to the new world
ondrejmirtes Jun 10, 2026
9065e5f
Migrate PrintHandler to the new world
ondrejmirtes Jun 10, 2026
6307fc9
Migrate CloneHandler to the new world
ondrejmirtes Jun 10, 2026
7c2715d
Migrate ExitHandler to the new world
ondrejmirtes Jun 10, 2026
fd2f8ff
Migrate ErrorSuppressHandler to the new world
ondrejmirtes Jun 10, 2026
ca003a2
Migrate EvalHandler to the new world
ondrejmirtes Jun 10, 2026
f4fd559
Migrate IncludeHandler to the new world
ondrejmirtes Jun 10, 2026
97ea4a2
Migrate CastStringHandler to the new world
ondrejmirtes Jun 10, 2026
a742db6
Migrate CastHandler to the new world
ondrejmirtes Jun 10, 2026
613f513
Migrate ClassConstFetchHandler to the new world
ondrejmirtes Jun 10, 2026
572065e
Migrate InterpolatedStringHandler to the new world
ondrejmirtes Jun 10, 2026
382caba
Migrate InstanceofHandler to the new world
ondrejmirtes Jun 10, 2026
f5659d9
Migrate BinaryOpHandler narrowing to the new world
ondrejmirtes Jun 10, 2026
46d235d
Migrate ArrayDimFetchHandler to the new world
ondrejmirtes Jun 10, 2026
5279d1a
Migrate IssetHandler to the new world
ondrejmirtes Jun 10, 2026
4478c2f
Migrate EmptyHandler to the new world
ondrejmirtes Jun 10, 2026
e19829c
Migrate CoalesceHandler to the new world
ondrejmirtes Jun 10, 2026
9e6d494
Migrate StaticPropertyFetchHandler to the new world
ondrejmirtes Jun 10, 2026
cc545f7
Type first-class callables in the NodeScopeResolver fast-path
ondrejmirtes Jun 10, 2026
f679d6c
Migrate MethodCallHandler to the new world
ondrejmirtes Jun 10, 2026
cc73ffc
Migrate StaticCallHandler to the new world
ondrejmirtes Jun 10, 2026
ba07b10
Migrate NewHandler to the new world
ondrejmirtes Jun 10, 2026
aa13b6c
Migrate YieldHandler to the new world
ondrejmirtes Jun 10, 2026
7a634a0
Migrate YieldFromHandler to the new world
ondrejmirtes Jun 10, 2026
cf41476
Migrate PipeHandler to the new world
ondrejmirtes Jun 10, 2026
f461883
Document the 21-handler migration batch in NEW_WORLD.md
ondrejmirtes Jun 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
660 changes: 660 additions & 0 deletions NEW_WORLD.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ parameters:
-
rawMessage: Casting to string something that's already string.
identifier: cast.useless
count: 3
count: 5
path: src/Analyser/MutatingScope.php

-
Expand Down
24 changes: 24 additions & 0 deletions src/Analyser/DirectInternalScopeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public function __construct(
private $nodeCallback,
private ConstantResolver $constantResolver,
private bool $fiber = false,
private bool $resultAware = false,
)
{
}
Expand All @@ -64,6 +65,8 @@ public function create(
$className = MutatingScope::class;
if ($this->fiber) {
$className = FiberScope::class;
} elseif ($this->resultAware) {
$className = ResultAwareScope::class;
}

return new $className(
Expand Down Expand Up @@ -120,6 +123,27 @@ public function toFiberFactory(): InternalScopeFactory
);
}

public function toResultAwareFactory(): InternalScopeFactory
{
return new self(
$this->container,
$this->reflectionProvider,
$this->initializerExprTypeResolver,
$this->expressionTypeResolverExtensionRegistryProvider,
$this->exprPrinter,
$this->typeSpecifier,
$this->propertyReflectionFinder,
$this->parser,
$this->phpVersion,
$this->attributeReflectionFactory,
$this->configPhpVersion,
$this->nodeCallback,
$this->constantResolver,
false,
true,
);
}

public function toMutatingFactory(): InternalScopeFactory
{
return new self(
Expand Down
67 changes: 62 additions & 5 deletions src/Analyser/ExprHandler/ArrayDimFetchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Analyser\ExpressionResult;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper;
use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
Expand All @@ -23,9 +24,12 @@
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_merge;

/**
Expand All @@ -35,6 +39,10 @@
final class ArrayDimFetchHandler implements ExprHandler
{

public function __construct(private DefaultNarrowingHelper $defaultNarrowingHelper)
{
}

public function supports(Expr $expr): bool
{
return $expr instanceof ArrayDimFetch;
Expand Down Expand Up @@ -86,8 +94,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
isAlwaysTerminating: $varResult->isAlwaysTerminating(),
throwPoints: $varResult->getThrowPoints(),
impurePoints: $varResult->getImpurePoints(),
truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
expr: $expr,
typeCallback: static fn (): Type => new NeverType(),
specifyTypesCallback: fn (Expr $e, MutatingScope $s, TypeSpecifierContext $ctx): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($e, $ctx),
);
}

Expand All @@ -97,7 +106,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$impurePoints = array_merge($dimResult->getImpurePoints(), $varResult->getImpurePoints());
$scope = $varResult->getScope();

$varType = $scope->getType($expr->var);
$varType = $varResult->getType();
if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) {
$throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode(
$stmt,
Expand All @@ -109,14 +118,62 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
)->getThrowPoints());
}

// a nullsafe var that can be null short-circuits this fetch too; its
// handler already produced the null-union — propagate one level, no
// recursive chain walking (NEW_WORLD.md §3.10)
$isShortcircuited = static function (Expr $e, MutatingScope $s) use ($varResult): bool {
if (!$e instanceof ArrayDimFetch) {
throw new ShouldNotHappenException();
}

return ($e->var instanceof Expr\NullsafePropertyFetch || $e->var instanceof Expr\NullsafeMethodCall)
&& TypeCombinator::containsNull($varResult->getTypeForScope($s));
};
$typeCallback = static function (Expr $e, MutatingScope $s) use ($varResult, $dimResult, $isShortcircuited, $nodeScopeResolver, $stmt): Type {
if (!$e instanceof ArrayDimFetch || $e->dim === null) {
throw new ShouldNotHappenException();
}

$varTypeForFetch = $varResult->getTypeForScope($s);
if ($isShortcircuited($e, $s)) {
$varTypeForFetch = TypeCombinator::removeNull($varTypeForFetch);
}

if (
!$varTypeForFetch->isArray()->yes()
&& (new ObjectType(ArrayAccess::class))->isSuperTypeOf($varTypeForFetch)->yes()
) {
// ArrayAccess: the offsetGet() synthetic, processed on demand
// (ResultAwareScope tier 4)
$fetchedType = $s->toResultAwareScope([], $nodeScopeResolver, $stmt, new ExpressionResultStorage())->getType(
new MethodCall(
new TypeExpr($varTypeForFetch),
new Identifier('offsetGet'),
[
new Arg($e->dim),
],
),
);
} else {
$fetchedType = $varTypeForFetch->getOffsetValueType($dimResult->getTypeForScope($s));
}

if ($isShortcircuited($e, $s)) {
return TypeCombinator::union($fetchedType, new NullType());
}

return $fetchedType;
};

return new ExpressionResult(
$scope,
hasYield: $dimResult->hasYield() || $varResult->hasYield(),
isAlwaysTerminating: $dimResult->isAlwaysTerminating() || $varResult->isAlwaysTerminating(),
throwPoints: $throwPoints,
impurePoints: $impurePoints,
truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
expr: $expr,
typeCallback: $typeCallback,
specifyTypesCallback: fn (Expr $e, MutatingScope $s, TypeSpecifierContext $ctx): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($e, $ctx),
);
}

Expand Down
56 changes: 55 additions & 1 deletion src/Analyser/ExprHandler/ArrayHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
use PHPStan\Analyser\ExpressionResult;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NewWorld;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
Expand All @@ -22,11 +24,14 @@
use PHPStan\Node\LiteralArrayItem;
use PHPStan\Node\LiteralArrayNode;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\CallableType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_key_exists;
use function array_merge;
use function count;
use function spl_object_id;

/**
* @implements ExprHandler<Array_>
Expand All @@ -37,6 +42,7 @@ final class ArrayHandler implements ExprHandler

public function __construct(
private InitializerExprTypeResolver $initializerExprTypeResolver,
private DefaultNarrowingHelper $defaultNarrowingHelper,
)
{
}
Expand Down Expand Up @@ -73,15 +79,19 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
{
$itemNodes = [];
$itemResults = [];
$hasYield = false;
$throwPoints = [];
$impurePoints = [];
$isAlwaysTerminating = false;
foreach ($expr->items as $arrayItem) {
$itemNodes[] = new LiteralArrayItem($scope, $arrayItem);
// the embedded item scope answers the rules' getType() asks — in the
// new world those must go through the fiber so stored results answer
$itemNodes[] = new LiteralArrayItem(NewWorld::isEnabled() ? $scope->toFiberScope() : $scope, $arrayItem);
$nodeScopeResolver->callNodeCallback($nodeCallback, $arrayItem, $scope, $storage);
if ($arrayItem->key !== null) {
$keyResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->key, $scope, $storage, $nodeCallback, $context->enterDeep());
$itemResults[spl_object_id($arrayItem->key)] = $keyResult;
$hasYield = $hasYield || $keyResult->hasYield();
$throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints());
Expand All @@ -90,6 +100,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
}

$valueResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->value, $scope, $storage, $nodeCallback, $context->enterDeep());
$itemResults[spl_object_id($arrayItem->value)] = $valueResult;
$hasYield = $hasYield || $valueResult->hasYield();
$throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints());
Expand All @@ -98,12 +109,55 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
}
$nodeScopeResolver->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage);

// each item type was captured at its own evaluation point in the sequence —
// resolving them on any single scope (the old world) cannot handle items
// with side effects like `[$b = 1, $b + 1, $b++]`
$typeCallback = function (Expr $e, MutatingScope $s) use ($itemResults): Type {
if (!$e instanceof Array_) {
throw new ShouldNotHappenException();
}

$type = $this->initializerExprTypeResolver->getArrayType($e, static function (Expr $inner) use ($itemResults, $s): Type {
$id = spl_object_id($inner);
if (array_key_exists($id, $itemResults)) {
return $itemResults[$id]->getTypeForScope($s);
}

// getArrayType only asks about item keys and values — guarded
// legacy bridge just in case (PHPSTAN_FNSR=0)
return $s->getType($inner);
});

if (
count($e->items) === 2
&& isset($e->items[0], $e->items[1])
&& $type->isCallable()->maybe()
) {
$isCallableCall = new FuncCall(
new FullyQualified('is_callable'),
[new Arg($e)],
);
$isCallableCallString = $s->getNodeKey($isCallableCall);
if (
array_key_exists($isCallableCallString, $s->expressionTypes)
&& $s->expressionTypes[$isCallableCallString]->getType()->isTrue()->yes()
) {
$type = TypeCombinator::intersect($type, new CallableType());
}
}

return $type;
};

return new ExpressionResult(
$scope,
hasYield: $hasYield,
isAlwaysTerminating: $isAlwaysTerminating,
throwPoints: $throwPoints,
impurePoints: $impurePoints,
expr: $expr,
typeCallback: $typeCallback,
specifyTypesCallback: fn (Expr $e, MutatingScope $s, TypeSpecifierContext $ctx): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($e, $ctx),
);
}

Expand Down
Loading
Loading