Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
456bd94
Improve line number preservation
rcalicdan Sep 1, 2026
02682e4
Refactor parseFileMetadata method visibility and add comprehensive te…
rcalicdan Sep 1, 2026
36efc5d
Fix classname edge case to be more linient on valid class string valu…
rcalicdan Sep 1, 2026
ffd338d
Enhance InlineChecker to support empty array initialization for array…
rcalicdan Sep 1, 2026
5c6dc7d
Add normalization for generic type arguments and enhance fallback for…
rcalicdan Sep 1, 2026
bed9193
Refactor ParamChecker to enhance callable parameter handling and add …
rcalicdan Sep 1, 2026
4a5f3f7
Enhance TemplateManager to support additional type variance checks an…
rcalicdan Sep 1, 2026
0042852
Increase HYBRID_SAMPLE_THRESHOLD to 128 for improved array validation…
rcalicdan Sep 1, 2026
6dacde5
Enhance ParamChecker and TemplateManager for improved template bindin…
rcalicdan Sep 2, 2026
a64e3ea
fix code styling
rcalicdan Sep 2, 2026
7112de9
Refactor CallableWrapper to safely check callability and update Ident…
rcalicdan Sep 2, 2026
440d81a
Enhance CallableWrapper to handle void return types correctly; add te…
rcalicdan Sep 2, 2026
2d9553f
Refactor TemplateManager to simplify union variance checks; add tests…
rcalicdan Sep 2, 2026
d17057d
Refactor TemplateManager variance checks for clarity; add FluentBuild…
rcalicdan Sep 2, 2026
7d4bd65
Update nikic/php-parser requirement to version 5.3 in composer.json
rcalicdan Sep 2, 2026
4e817c9
Refactor ContractParser to enhance compatibility checks for construct…
rcalicdan Sep 2, 2026
580e4ac
Refactor ParamChecker to improve generic template inference; add test…
rcalicdan Sep 2, 2026
8d4e629
Refactor ContractParser to streamline variadic parameter handling; ad…
rcalicdan Sep 3, 2026
9e0368d
Refactor ParamChecker, Config, ArrayValidator, and GenericValidator t…
rcalicdan Sep 3, 2026
fc10524
Refactor ReturnChecker to utilize TemplateManager for variance checks…
rcalicdan Sep 3, 2026
0cfbf35
Enhance TemplateManager variance checks for UnionTypeNode; add Generi…
rcalicdan Sep 3, 2026
6fd6f91
Refactor TemplateManager variance checks for intersection types; simp…
rcalicdan Sep 3, 2026
a10907c
Add tests for normal union, intersection, and DNF subtyping; implemen…
rcalicdan Sep 3, 2026
6f65970
Fix TypeFormatter to handle float values correctly in formatGivenValu…
rcalicdan Sep 4, 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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"require": {
"php": "^8.1",
"phpstan/phpdoc-parser": "^2.0",
"nikic/php-parser": "^5.0"
"nikic/php-parser": "^5.3"
},
"require-dev": {
"laravel/pint": "^1.10",
Expand Down
106 changes: 78 additions & 28 deletions src/Contract/ContractParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -619,16 +619,10 @@ private static function parseFunction(\ReflectionFunction $ref): array

foreach (DocblockExtractor::getParamTags($phpDocNode) as $paramName => $paramTag) {
$type = $paramTag->type;
$isVariadic = $paramTag->isVariadic || ($baseParamVariadic[$paramName] ?? false);
if (
$isVariadic
&& ! ($type instanceof ArrayTypeNode)
&& ! ($type instanceof GenericTypeNode && \in_array(strtolower($type->type->name), ['array', 'list', 'iterable', 'traversable', 'non-empty-array', 'non-empty-list'], true))
) {
$type = new ArrayTypeNode($type);
}

$pObj = $baseParamObjects[$paramName] ?? null;

$type = self::wrapVariadicParameterType($type, $paramTag->isVariadic, $baseParamVariadic[$paramName] ?? false, $pObj);

if (
Config::isRespectNativeNullabilityEnabled()
&& $pObj !== null
Expand Down Expand Up @@ -796,16 +790,10 @@ private static function parseMethodHierarchyDocs(

if ($targetParamName !== null && ! isset($types[$targetParamName])) {
$type = $paramTag->type;
$isVariadic = $paramTag->isVariadic || ($baseParamVariadic[$targetParamName] ?? false);
if (
$isVariadic
&& ! ($type instanceof ArrayTypeNode)
&& ! ($type instanceof GenericTypeNode && \in_array(strtolower($type->type->name), ['array', 'list', 'iterable', 'traversable', 'non-empty-array', 'non-empty-list'], true))
) {
$type = new ArrayTypeNode($type);
}

$pObj = $baseParamObjects[$targetParamName] ?? null;

$type = self::wrapVariadicParameterType($type, $paramTag->isVariadic, $baseParamVariadic[$targetParamName] ?? false, $pObj);

if (
Config::isRespectNativeNullabilityEnabled()
&& $pObj !== null
Expand Down Expand Up @@ -897,10 +885,15 @@ private static function applyConstructorPromotionFallback(
$paramName = $p->getName();

if (! isset($types[$paramName]) && $declaringClass->hasProperty($paramName)) {
$propertyRef = $declaringClass->getProperty($paramName);

// For non-promoted parameters, ensure constructor param and property native types are compatible
if (! self::areConstructorParamAndPropertyCompatible($p, $propertyRef)) {
continue;
}

$className = $declaringClass->getName();
$stubDoc = StubManager::getPropertyDoc($className, $paramName);

$propertyRef = $declaringClass->getProperty($paramName);
$propDoc = $stubDoc ?? $propertyRef->getDocComment();

if ($propDoc !== false && $propDoc !== null) {
Expand All @@ -926,14 +919,7 @@ private static function applyConstructorPromotionFallback(
}
}

$isVariadic = $p->isVariadic();
if (
$isVariadic
&& ! ($resolvedProp instanceof ArrayTypeNode)
&& ! ($resolvedProp instanceof GenericTypeNode && \in_array(strtolower($resolvedProp->type->name), ['array', 'list', 'iterable', 'traversable', 'non-empty-array', 'non-empty-list'], true))
) {
$resolvedProp = new ArrayTypeNode($resolvedProp);
}
$resolvedProp = self::wrapVariadicParameterType($resolvedProp, false, $p->isVariadic(), $p);

if (
Config::isRespectNativeNullabilityEnabled()
Expand All @@ -954,6 +940,70 @@ private static function applyConstructorPromotionFallback(
}
}

/**
* Checks whether an un-promoted constructor parameter is compatible with a class property.
*/
private static function areConstructorParamAndPropertyCompatible(
\ReflectionParameter $p,
\ReflectionProperty $propertyRef
): bool {
if ($p->isPromoted()) {
return true;
}

if ($p->hasType() && $propertyRef->hasType()) {
$pType = (string) $p->getType();
$propType = (string) $propertyRef->getType();

$normP = strtolower(ltrim($pType, '?'));
$normProp = strtolower(ltrim($propType, '?'));

if ($normP !== $normProp) {
return false;
}
}

return true;
}

/**
* Wraps a variadic parameter type into an ArrayTypeNode, accounting for variadic arrays/iterables.
*/
private static function wrapVariadicParameterType(
TypeNode $type,
bool $tagIsVariadic,
bool $nativeIsVariadic,
?\ReflectionParameter $reflectionParam = null
): TypeNode {
if (! $tagIsVariadic && ! $nativeIsVariadic) {
return $type;
}

$isNativeArrayOrIterable = false;
if ($reflectionParam !== null && $reflectionParam->hasType()) {
$nativeType = $reflectionParam->getType();
if ($nativeType instanceof \ReflectionNamedType) {
$nativeName = strtolower($nativeType->getName());
$isNativeArrayOrIterable = \in_array($nativeName, ['array', 'iterable'], true);
}
}

// If native parameter is array ...$items, each argument is an array, so wrap in ArrayTypeNode
if ($isNativeArrayOrIterable) {
return new ArrayTypeNode($type);
}

// If DocBlock type is not already an ArrayTypeNode or list/array GenericTypeNode
if (
! ($type instanceof ArrayTypeNode)
&& ! ($type instanceof GenericTypeNode && \in_array(strtolower($type->type->name), ['array', 'list', 'iterable', 'traversable', 'non-empty-array', 'non-empty-list'], true))
) {
return new ArrayTypeNode($type);
}

return $type;
}

/**
* Checks if a scalar refinement type is compatible with a native PHP builtin type.
*/
Expand Down
28 changes: 28 additions & 0 deletions src/Internal/Checker/InlineChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ public static function checkVariable(mixed $value, string $typeString, string $v
return $value;
}

if ($value === [] && $varName !== 'return' && self::isArrayShapeType($typeNode)) {
return $value;
}

$context = ($varName === 'return') ? 'Return value' : "Variable \$$varName";

if ($typeNode instanceof CallableTypeNode || ($typeNode instanceof IdentifierTypeNode && strtolower($typeNode->name) === 'callable')) {
Expand Down Expand Up @@ -166,6 +170,30 @@ public static function checkVariable(mixed $value, string $typeString, string $v
return $value;
}

/**
* Checks if a TypeNode is or contains an ArrayShapeNode.
*/
private static function isArrayShapeType(TypeNode $node): bool
{
if ($node instanceof ArrayShapeNode) {
return true;
}

if ($node instanceof NullableTypeNode) {
return self::isArrayShapeType($node->type);
}

if ($node instanceof UnionTypeNode) {
foreach ($node->types as $subType) {
if (self::isArrayShapeType($subType)) {
return true;
}
}
}

return false;
}

/**
* Evaluates class property validation dynamically based on configuration.
*/
Expand Down
Loading
Loading