Skip to content

Statically analyse attributes #10443

@GottemHams

Description

@GottemHams

Feature request

As per the earlier discussion:

Commit ondrejmirtes/BetterReflection@30fd205 deprecates creating a new attribute instance, but there's no sane replacement at the moment. You currently need an actual instance if you want to check the attribute's properties though. Combined example (without namespaces to keep it minimal):

#[Attribute(Attribute::TARGET_CLASS)]
class MyAttrib {
	/** @var class-string */
	public string $my_class_name;

	/** @param class-string $my_class_name */
	public function __construct(string $my_class_name) {
		$this->my_class_name = $my_class_name;
	}
}

#[MyAttrib(Foo::class)]
class Bar {
}

// Custom PHPStan rule -> processNode() function
$class_reflection = $scope->getClassReflection();
$class_attributes = $class_reflection->getNativeReflection()->getAttributes(MyAttrib::class);
if(count($class_attributes)) {
	$attribute_instance = $class_attributes[0]->newInstance(); // PHPStan reports deprecation here
	// Do something with $attribute_instance->my_class_name;
}

Since PHPStan uses a (mostly) static reflection engine, the only "real" way would be to also analyse these attributes statically. So with #[MyAttrib(Foo::class)]:

  1. PHPStan sees it references an attribute class named MyAttrib and follows it to the constructor.
  2. The argument is string $my_class_name, so PHPStan internally stores something saying my_class_name = Foo::class.
  3. Instead of $class_reflection->getNativeReflection()->getAttributes() from within a rule, you can use just $class_attributes = $class_reflection->getAttributes(MyAttrib::class) (no getNativeReflection()).
  4. Then ask PHPStan for information about the argument: $class_attributes[0]->getArgument('my_class_name')->getValue(), or even typed like getStringValue(). It could also return an array via the plural getArguments(), although I'm not sure if someone would ever need to loop over these properties instead of just accessing them directly.

Some potential issues:

  1. What if the attribute's constructor has argument string $foo but the class property is string $bar? I think realistically people will tend to use equal names, or even better: constructor property promotion. The documentation could simply mention that only the constructor's argument names are used, or perhaps PHPStan could emit an error/warning when you run ->getArgument('bar') (since no argument with that name actually exists). Or it could simply return NULL and people will have to check for that in their own rules, then emit an error as needed. Having PHPStan generate an error/warning automatically would help people with pre-existing rules to "migrate" to the new style though.
  2. What if the constructor modifies the passed argument? E.g. it accepts string $foo but stores it as $this->foo = intval($foo). In my opinion this is kind of misusing attributes, because now your attribute "annotation" no longer matches what sort of information the attribute actually gives you. Attributes themselves don't need to do anything besides just storing some key-value pairs really, so having any logic in them should probably be avoided. PHP intended for them to provide machine-readable metadata only, why mess with the values?
  3. PHPStan will likely be unable to return getAttributes(MyAttrib::class) actually typed as MyAttrib[] (since there's no actual instance of that class), which means LSP engines can no longer inform you about existing properties (and their types) from MyAttrib. I currently have /** @var MyAttrib $attribute_instance */ for this, but using that for getAttributes(...)[0] would not work because ->my_class_name still doesn't exist. Maybe PHPStan could simply create an stdClass with properties based on the attribute arguments, also removing the need for ->getValue() and even solving the problem of people having to "migrate" their custom rules? So basically emulate creating an instance, populated with only static data. I personally try to avoid free-form objects though, so not quite sure how PHPStan should realistically deal with this (would it even fit within its static data model?). ;_;

Did PHPStan help you today? Did it make you happy in any way?

PHPStan helps me every day yo. :>

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions