Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions build/php-85.neon
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ parameters:
count: 1
path: ../src/Testing/TestCaseSourceLocatorFactory.php

-
rawMessage: 'Call to deprecated method setAccessible() of class ReflectionProperty.'
identifier: method.deprecated
count: 1
path: ../tests/PHPStan/Analyser/StmtHandler/ForeachHandlerTest.php

-
rawMessage: 'Call to deprecated method setAccessible() of class ReflectionProperty.'
identifier: method.deprecated
Expand Down
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
final class TurboExtensionEnabler
{

public const EXPECTED_EXTENSION_VERSION = '56d1522';
public const EXPECTED_EXTENSION_VERSION = '341ab11';

private static bool $active = false;

Expand Down
48 changes: 48 additions & 0 deletions tests/PHPStan/Analyser/PhpDocsResolverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PhpParser\Comment\Doc;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\Type\VerbosityLevel;
use function array_map;

class PhpDocsResolverTest extends PHPStanTestCase
{

public function testParameterNamesFromPhpBuiltNodes(): void
{
$file = __DIR__ . '/data/php-docs-resolver-parameter-names.php';
require_once $file;

$reflectionProvider = self::createReflectionProvider();
$scope = self::createScopeFactory($reflectionProvider, self::getContainer()->getService('typeSpecifier'))
->create(ScopeContext::create($file))
->enterClass($reflectionProvider->getClass('PhpDocsResolverParameterNames\Foo'));

// the names are PHP literals (interned strings), unlike the ones
// the parser allocates
$node = new ClassMethod(new Identifier('doFoo'), [
'params' => [
new Param(new Variable('a')),
new Param(new Variable('count')),
],
], [
'comments' => [
new Doc("/**\n\t * @param non-empty-string \$a\n\t * @param positive-int \$count\n\t */"),
],
]);

$phpDocParameterTypes = self::getContainer()->getByType(PhpDocsResolver::class)->getPhpDocs($scope, $node)[1];

$this->assertSame([
'a' => 'non-empty-string',
'count' => 'int<1, max>',
], array_map(static fn ($type) => $type->describe(VerbosityLevel::precise()), $phpDocParameterTypes));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser\StmtHandler;

use PhpParser\Node\Expr;
use PHPStan\Analyser\ConditionalExpressionHolder;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Type\Type;
use function count;

/**
* A MutatingScope subclass overriding addConditionalExpressions(), so the
* native engine has to hand the holder tables over through a PHP call.
* enterForeach() and mergeWith() answer $this to keep this subclass the
* scope ForeachHandler narrows.
*/
final class ConditionalExpressionsRecordingScope extends MutatingScope
{

/** @var list<array{string, int}> */
public array $added = [];

public function enterForeach(MutatingScope $originalScope, Expr $iteratee, Type $iterateeType, Type $nativeIterateeType, string $valueName, ?string $keyName, bool $valueByRef): MutatingScope
{
return $this;
}

public function mergeWith(?MutatingScope $otherScope, bool $preserveVacuousConditionals = false): MutatingScope
{
return $this;
}

/**
* @param ConditionalExpressionHolder[] $conditionalExpressionHolders
*/
public function addConditionalExpressions(string $exprString, array $conditionalExpressionHolders): MutatingScope
{
$this->added[] = [$exprString, count($conditionalExpressionHolders)];

return $this;
}

}
64 changes: 64 additions & 0 deletions tests/PHPStan/Analyser/StmtHandler/ForeachHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser\StmtHandler;

use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\Foreach_;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\NoopNodeCallback;
use PHPStan\Analyser\ScopeContext;
use PHPStan\Analyser\StatementContext;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPUnit\Framework\Attributes\DataProvider;
use ReflectionClass;

class ForeachHandlerTest extends PHPStanTestCase
{

public static function dataConstantArrayConditionalHolders(): iterable
{
// no keys: both holder tables stay the empty array literal
yield [new ConstantArrayType([], []), 0];
yield [new ConstantArrayType([new ConstantStringType('a')], [new ConstantIntegerType(1)]), 1];
}

#[DataProvider('dataConstantArrayConditionalHolders')]
public function testConstantArrayConditionalHoldersReachScopeOverride(ConstantArrayType $iterateeType, int $expectedHolders): void
{
$container = self::getContainer();
$scope = self::createScopeFactory(self::createReflectionProvider(), $container->getService('typeSpecifier'))
->create(ScopeContext::create(__FILE__))
->assignVariable('a', $iterateeType, $iterateeType, TrinaryLogic::createYes());
// the scope factory only creates MutatingScope: rebuild the scope as
// the subclass from its constructor arguments
$args = [];
$reflection = new ReflectionClass(MutatingScope::class);
foreach ($reflection->getMethod('__construct')->getParameters() as $parameter) {
$property = $reflection->getProperty($parameter->getName());
$property->setAccessible(true);
$args[] = $property->getValue($scope);
}
$scope = new ConditionalExpressionsRecordingScope(...$args);

$container->getByType(ForeachHandler::class)->processStmt(
$container->getByType(NodeScopeResolver::class),
new Foreach_(new Variable('a'), new Variable('v'), ['keyVar' => new Variable('k')]),
$scope,
new ExpressionResultStorage(),
new NoopNodeCallback(),
StatementContext::createDeep(),
);

$this->assertSame([
['$v', $expectedHolders],
['$a[$k]', $expectedHolders],
], $scope->added);
}

}
16 changes: 16 additions & 0 deletions tests/PHPStan/Analyser/data/php-docs-resolver-parameter-names.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace PhpDocsResolverParameterNames;

class Foo
{

/**
* @param non-empty-string $a
* @param positive-int $count
*/
public function doFoo($a, $count): void
{
}

}
6 changes: 4 additions & 2 deletions turbo-ext/src/ConstantArrayType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5177,9 +5177,11 @@ class ConstantArrayType

zv::Val keyValueZv = keyValue(keyType);
if (UNEXPECTED(keyValueZv.isUndef())) return zv::Val();
optionalKeysCopy.separate();
/* iterated over a snapshot: the twin's foreach reads a copy */
/* iterated over a snapshot: the twin's foreach reads a copy;
* taken before separating, so the deletes below go into a table
* of their own */
zv::Arr snapshot = zv::Arr::copyOfTable(optionalKeysCopy.table());
optionalKeysCopy.separate();
for (zv::ArrayEntry optionalEntry : snapshot.arrRef()) {
zval *key = optionalEntry.value().deref().raw();
bool keep = Z_TYPE_P(key) != IS_LONG || i != Z_LVAL_P(key);
Expand Down
2 changes: 1 addition & 1 deletion turbo-ext/src/PhpDocsResolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ class PhpDocsResolver
return zv::Val();
}
names.separate();
Z_ADDREF_P(name);
Z_TRY_ADDREF_P(name);
zend_string *key = entry.stringKeyOrNull();
if (key != NULL) {
zend_hash_update(names.table(), key, name);
Expand Down
8 changes: 1 addition & 7 deletions turbo-ext/src/VariableLivenessResolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1365,13 +1365,7 @@ class VariableLivenessResolver
zv::Str slotName = offsetKey(view.offset.raw());
selectedKeys = emptyArray();
HashTable *keys = innerTableByKey(keysBySlot, slotName.get());
zval keysValue;
if (keys != NULL) {
ZVAL_ARR(&keysValue, keys);
Z_ADDREF(keysValue);
} else {
ZVAL_EMPTY_ARRAY(&keysValue);
}
zval keysValue = (keys != NULL ? zv::Arr::copyOfTable(keys) : zv::Arr::empty()).take();
SEPARATE_ARRAY(selectedKeys.raw());
zend_hash_update(Z_ARRVAL_P(selectedKeys.raw()), slotName.get(), &keysValue);
} else {
Expand Down
10 changes: 9 additions & 1 deletion turbo-ext/src/zv.h
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,15 @@ class Args
static zend_always_inline void set(zval *slot, const zval *value) { ZVAL_COPY_VALUE(slot, value); }
static zend_always_inline void set(zval *slot, zend_object *value) { ZVAL_OBJ(slot, value); }
static zend_always_inline void set(zval *slot, zend_string *value) { ZVAL_STR(slot, value); }
static zend_always_inline void set(zval *slot, HashTable *value) { ZVAL_ARR(slot, value); }
/* immutable tables (a PHP [] literal) are wrapped non-refcounted, like
* ZVAL_EMPTY_ARRAY: the call's addref of its arguments must not touch them */
static zend_always_inline void set(zval *slot, HashTable *value)
{
ZVAL_ARR(slot, value);
if (GC_FLAGS(value) & IS_ARRAY_IMMUTABLE) {
Z_TYPE_INFO_P(slot) = IS_ARRAY;
}
}
static zend_always_inline void set(zval *slot, bool value) { ZVAL_BOOL(slot, value); }
static zend_always_inline void set(zval *slot, zend_long value) { ZVAL_LONG(slot, value); }
static zend_always_inline void set(zval *slot, double value) { ZVAL_DOUBLE(slot, value); }
Expand Down
Loading