Skip to content

[Strict] Skip class re-parse for typed default property in UninitializedPropertyAnalyzer#8101

Merged
TomasVotruba merged 1 commit into
mainfrom
precheck-uninitialized-property
Jun 28, 2026
Merged

[Strict] Skip class re-parse for typed default property in UninitializedPropertyAnalyzer#8101
TomasVotruba merged 1 commit into
mainfrom
precheck-uninitialized-property

Conversation

@TomasVotruba

Copy link
Copy Markdown
Member

What

UninitializedPropertyAnalyzer::isUninitialized() resolved the declaring class into a full AST via AstResolver->resolveClassFromName() for every property fetch, only to then check the property's default and whether it's assigned in the constructor.

A typed property with an explicit default (private array $items = [];) is always initialized — it can never be in an uninitialized state — so the answer is false without parsing anything. This adds a ReflectionProvider fast-path that returns early in that case, before the parse:

if ($this->hasTypedDefaultProperty($className, $propertyName)) {
    return false;
}
private function hasTypedDefaultProperty(string $className, string $propertyName): bool
{
    if (! $this->reflectionProvider->hasClass($className)) {
        return false;
    }

    $classReflection = $this->reflectionProvider->getClass($className);
    if (! $classReflection->hasNativeProperty($propertyName)) {
        return false;
    }

    $nativeReflectionProperty = $classReflection->getNativeProperty($propertyName)->getNativeReflection();

    // an untyped property has an implicit null default, which is not an explicit initialization
    return $nativeReflectionProperty->hasType() && $nativeReflectionProperty->hasDefaultValue();
}

This is a partial improvement: when the property has no default (or isn't typed), the analyzer still falls through to AstResolver + ConstructorAssignDetector, because "is this property assigned in the constructor?" is flow analysis that reflection cannot answer. Only the always-initialized case is short-circuited.

Why the hasType() guard

An untyped property (public $items;) reports an implicit null default — ReflectionProperty::hasDefaultValue() returns true even though there is no explicit = .... Guarding on hasType() keeps untyped properties on the original path so behaviour is unchanged.

Tests

Existing fixtures already cover both branches and pass unchanged:

  • property_with_default_value (typed + default → fast-path)
  • may_uninitialized_property_no_default_value (typed, no default → AST path)

Added skip_untyped_property to lock the untyped-property behaviour (left unchanged).

vendor/bin/phpunit rules-tests/Strict/Rector/Empty_/DisallowedEmptyRuleFixerRector → 18/18 green. ECS + full PHPStan level 8 clean.

…zedPropertyAnalyzer

A typed property with an explicit default value is always initialized, so
empty()/isset() narrowing never applies to it. Detect that case up front
via ReflectionProvider and return early, avoiding the heavy
AstResolver::resolveClassFromName() parse of the declaring class.

The hasType() guard keeps untyped properties (implicit null default) on
the existing path, preserving current behaviour.
@TomasVotruba TomasVotruba merged commit 34c76d8 into main Jun 28, 2026
65 checks passed
@TomasVotruba TomasVotruba deleted the precheck-uninitialized-property branch June 28, 2026 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant