Skip to content

[Bug]: Class template property is checked against the template name when the constructor is inherited from a non-generic parent #112

Closed

Description

@fdaviddpt

Summary

A class declares @template TEntity and a property @var TEntity. If the class's constructor is inherited from a parent above it (a parent with no @template), assigning anything to the property fails as if TEntity were a class name:

Property App\TextField::$entity must be of type TEntity, stdClass given

TEntity is unbound, so any value should be accepted (PHPStan treats it as mixed). Move the constructor into the templated class, or remove it, and the same assignment passes.

This is a different trigger from #104 (fixed in v0.11.2): #104's repro has no constructor and passes on 0.11.3; whether the declaring class is inside include does not matter here, the repro fails with everything included.

Playground

Open in playground — output: Property App\TextField::$entity must be of type TEntity, stdClass given

<?php

declare(strict_types=1);

namespace App;

abstract class Base
{
    public function __construct()
    {
    }
}

/**
 * @template TEntity
 */
abstract class Field extends Base
{
    /** @var TEntity */
    public $entity;
}

/**
 * @template TEntity
 * @extends Field<TEntity>
 */
abstract class InputField extends Field
{
}

final class TextField extends InputField
{
}

try {
    $field = new TextField();
    $field->entity = new \stdClass();
    echo "construct/assign: OK\n";
} catch (\Throwable $e) {
    echo $e->getMessage(), "\n";
}

Actual

Property App\TextField::$entity must be of type TEntity, stdClass given

Expected

construct/assign: OK

Controls

Any one of these makes it pass:

  • delete Base::__construct;
  • move the constructor into Field (the class that declares @template TEntity);
  • redeclare __construct in TextField.

The InputField middle class is not needed (TextField extends Field also fails); it is there because that is the shape we hit.

Where it seems to come from

InlineChecker::checkProperty() asks propertyUsesTemplates() / substitutePropertyGenerics(), which read the class templates through DocblockParser::parse($className . '::__construct')['classTemplates']. For an inherited constructor, parseMethod() takes the target class from $ref->class, i.e. the class that declares __construct (Base), which has no templates. classTemplates comes back empty, the template is not substituted, and the validator resolves TEntity as a class. Passing the requested class into parseMethod() (or reading the templates with the class-level parser instead of via the constructor) fixes the repro locally.

Versions

v0.11.3 and dev-main @ 4e4244e (local project, PHP 8.3), and the playground.

Context

Our framework's base component class owns the constructor; every form field extends it through a @template TEntity field base, so every $field->entity = $entity in checked code reports a violation (about 200 per module run).

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions