Skip to content

1.7 incorrectly detects prototypes from PHP extensions #7294

@kallesommernielsen

Description

@kallesommernielsen

Bug report

Hi,

After I got the upgrade notice of PHPStan 1.7.0 via Dependabot last night, I have started to notice what seems like a regression in 1.7 when it comes to the variance check in PHPStan. It seems that when PHPStan's static reflection attempts to detect the prototype of a method which was declared internally, PHPStan will automatically add null to the signature on PHP 7.4 when an internal method has a default value.

For reference, the setup is:

  • PHPStan 1.7.0
  • PHP 7.4.29
  • Phalcon 4.1.2

Code snippet that reproduces the problem

<?php

declare(strict_types=1);

// If this block is removed and instead the Phalcon extension is loaded, then 
// PHPStan will incorrectly detect an error on line 22 because the class 
// implementation is not contravariannt with the interface, complaining that 
// parameter #1 ($attributes) of 'array' is not contravariant with 'array|null'
namespace Phalcon\Forms\Element {
	interface ElementInterface {
		/**
		 * @param array<string, string> $attributes
		 */
		public function render(array $attributes = []): string;
	}
}

namespace Userland\Library\Forms {
	use Phalcon\Forms\Element\ElementInterface;
	
	class TextElement implements ElementInterface {
		public function render(array $attributes = []): string {
			return '<input type="text">';
		}
	}
}

namespace {
	use Userland\Library\Forms\TextElement;
	
	echo (new TextElement())->render();
}

Expected output

I expect the code to work as it was prior to 1.7

Other

I dug a little bit into it, and while PHPStan uses BetterReflection, it's detection when it comes to internal methods must be broken somewhere in the chain. Regular Reflection will work, though hessitantly when it comes to default values on 7.4:

F:\>php -r "$r = new \ReflectionClass('\Phalcon\Forms\Element\ElementInterface'); $p = $r->getMethod('render')->getParameters()[0]; var_dump($p->getType()->allowsNull()); var_dump($p->getDefaultValue());"
bool(false)
PHP Fatal error:  Uncaught ReflectionException: Cannot determine default value for internal functions in Command line code:1
Stack trace:
#0 Command line code(1): ReflectionParameter->getDefaultValue()
#1 {main}
  thrown in Command line code on line 1

Fatal error: Uncaught ReflectionException: Cannot determine default value for internal functions in Command line code:1
Stack trace:
#0 Command line code(1): ReflectionParameter->getDefaultValue()
#1 {main}
  thrown in Command line code on line 1
$r = new \ReflectionClass('\Phalcon\Forms\Element\ElementInterface');
$p = $r->getMethod('render')->getParameters()[0]; 

var_dump($p->getType()->allowsNull());
var_dump($p->getDefaultValue());

I assume this is where PHPStan or BetterReflection will add null as a suitable default value to the prototype for $attributes combined with the fact that PHP reports $attributes to be optional

The output from PHPStan is something along the lines of this:

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 
  Line   src\UI\Forms\Button.php                                                                                                                                                                                      
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 
  36     Parameter #1 $attributes (array) of method Project\UI\Forms\Button::render() is not contravariant with parameter #1 $attributes (array|null) of method Phalcon\Forms\Element\ElementInterface::render().  
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 

Relevant php --rc output of the above refenced methods:

    Method [ <internal:phalcon> abstract public method render ] {

      - Parameters [1] {
        Parameter #0 [ <optional> array $attributes ]
      }
      - Return [ string ]
    }

... and the same output for PHP 8.0 (where a default value can be obtained via \ReflectionParameter::getDefaultValue() for internal functionality):

    Method [ <internal:phalcon> abstract public method render ] {

      - Parameters [1] {
        Parameter #0 [ <optional> array $attributes = [] ]
      }
      - Return [ string ]
    }

I thought about solving this with stubs, but after some deliberation I am of the impression that this is a bug that should be solved upstream.

Other notes:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions