Skip to content

Add ClosureFromCallableToFirstClassCallableRector rule #7071

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

devnix
Copy link
Contributor

@devnix devnix commented Jul 17, 2025

@devnix devnix force-pushed the ClosureFromCallableToFirstClassCallableRector branch from ee6d875 to 0a4cbea Compare July 18, 2025 07:48
@@ -23,6 +24,7 @@
SpatieEnumClassToEnumRector::class,
SpatieEnumMethodCallToEnumConstRector::class,
NullToStrictStringFuncCallArgRector::class,
ClosureFromCallableToFirstClassCallableRector::class,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to use CodingStyle namespace for this, and no need to register to config set, as it mostly by preference, use of first class callable mostly cause error on unit tests and we skip that over time on projects

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samsonasik

I prefer to use CodingStyle namespace for this, and no need to register to config set

No problem! It should be a pretty safe refactor for PHP 8.1. There are a lot of upgrade rules that I could consider too as more opinionated towards coding style, so I'm a little bit lost here. To me, Rector promotes you to use new language features by applying them to your entire codebase.

use of first class callable mostly cause error on unit tests and we skip that over time on projects

How does that happen? As far as I understand, both are closures, both are bindable, and I can't think of a case when this rule could break something:
image

If you are aware of a case when this could be a problem, could you share a snippet so I can try to be sure that it doesn't do anything unexpected on specific Closure::fromCallable usages?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit test may compare its arguments over its value, [DateTime', 'creatFromInterface] which is an array, vs ... which an variadic place holder.

@devnix
Copy link
Contributor Author

devnix commented Jul 20, 2025

I've not removed the rule from the 8.1 ruleset as I'm not sure about in which level of CodingStyle it should be. To me, it's a very safe rule 🙂

Where would you place it exactly?

@devnix devnix marked this pull request as ready for review July 20, 2025 12:01
@devnix devnix changed the title Created ClosureFromCallableToFirstClassCallableRector rule, fixture… Add ClosureFromCallableToFirstClassCallableRector rule Jul 20, 2025

if ($arg->value instanceof Node\Scalar\String_) {
return new Node\Expr\FuncCall(
new Node\Name($this->prefixRootNamespace($arg->value->value)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FullyQualified node can be used instead of manual prefix check

@@ -0,0 +1,23 @@
<?php

use SomeNamespace\Foo;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use SomeNamespace\Foo;
namespace Rector\Tests\Php81\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this help testing relative namespaces?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless required by rule, fixture namespace should psr4 per defined in composer.json

-----
<?php

use SomeNamespace\Foo;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
use SomeNamespace\Foo;
namespace Rector\Tests\Php81\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture;

@@ -0,0 +1,21 @@
<?php

namespace Foo;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
namespace Foo;
namespace Rector\Tests\Php81\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture;

-----
<?php

namespace Foo;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
namespace Foo;
namespace Rector\Tests\Php81\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture;


namespace Foo;

\trim(...);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is fine without \ prefix, you should can use combination of:

  • $this->getName($funcCall) to verify only short name.
  • use of :
        $funcReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node);

and verify if that instanceof NativeFunctionReflection

        if (! $funcReflection instanceof \PHPStan\Reflection\Native\NativeFunctionReflection\NativeFunctionReflection) {

        }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add test needs check when function defined inside the namespace:

namespace Foo;

function bar()
{
}

Closure::fromCallable('bar');

which should be changed without prefix:

namespace Foo;

function bar()
{
}

-Closure::fromCallable('bar');
+bar(...);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is not valid: https://3v4l.org/NTdvJ#vnull
AFAIK, strings referring to functions can't be relative and must be FQDN: https://3v4l.org/DFr8s#vnull

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I think the native trim is still valid without \\

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding to this comment:

this is fine without \ prefix, you should can use combination of:

  • $this->getName($funcCall) to verify only short name.
  • use of :
        $funcReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node);

and verify if that instanceof NativeFunctionReflection

        if (! $funcReflection instanceof \PHPStan\Reflection\Native\NativeFunctionReflection\NativeFunctionReflection) {

        }

I've grinded my gears and I don't find a way to get what you want. I guess that the ' $node' I should pass to that resolver is the FuncCall that I'm creating and returning.

In that case, this example

namespace Rector\Tests\Php81\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture {
    function trim()
    {}

    \Closure::fromCallable('Rector\Tests\Php81\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture\trim');
    \Closure::fromCallable('trim');
}

becomes this one:

namespace Rector\Tests\Php81\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture {
    function trim()
    {}

    \Rector\Tests\Php81\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture\trim(...);
    trim(...);
}

while it should be

namespace Rector\Tests\Php81\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture {
    function trim()
    {}

    \Rector\Tests\Php81\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture\trim(...);
    \trim(...);
}

The new node I'm creating, when evaluated by that resolver, is not aware that it will be inside a namespace.

I'm personally not bothered by having unnecessary \ in some cases, and I know about a lot of cases that create nodes with an unnecessary FQDN prefixed with the root \. That's why I rely on a formatting tool.

Maybe it's something doable and I'm just not seeing it. If there's a way, I would love to learn about it 🙏🏻

@devnix
Copy link
Contributor Author

devnix commented Jul 22, 2025

I've taken a look at all your comments @samsonasik, please let me know if I can improve it in any other way. Thanks for having a look! <3

@samsonasik
Copy link
Member

Ok, just unregister from config set, I still don't want it to exists by php set for now, user can test try in the wild :)

@devnix
Copy link
Contributor Author

devnix commented Jul 22, 2025

I will move it to the CodingStyle namespace then, since there's a rule that forces me to register the service if it lives in the PHP 8.1 namespace:

 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  rules/Php81/Rector/FuncCall/ClosureFromCallableToFirstClassCallableRector.php:17
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  - '#Register "Rector\\Php81\\Rector\\FuncCall\\ClosureFromCallableToFirstClassCallableRector" service to "php81\.php" config set#'
  🪪 rector.upgradeDowngradeRegisteredInSet
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

@samsonasik
Copy link
Member

Yep, coding style is better imo :)

@devnix devnix force-pushed the ClosureFromCallableToFirstClassCallableRector branch from b49d20a to ae9a5de Compare July 22, 2025 14:34
@devnix
Copy link
Contributor Author

devnix commented Jul 22, 2025

I hoped it was a problem only on my machine, but it replicates on the CI too 😰

In the PHP 8.1 namespace the tests were green, and in the process of moving it to the new namespace, the tests are failing like the rule is not being applied.

I can't xdebug it launching the whole test suite. Funny enough, if I run the ClosureFromCallableToFirstClassCallableRectorTest only, the fixtures are correctly transformed and the test is green.

@samsonasik
Copy link
Member

That usually happen when there is typo or invalid character case due to file name vs class name vs folder name due to copy paste and replace

use Rector\Config\RectorConfig;

return RectorConfig::configure()
->withRules([ClosureFromCallableToFirstClassCallableRector::class]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You seems need to add ->withPhpVersion() here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooooof, I've just literally found it right now! thank you anyways 😄

@devnix devnix force-pushed the ClosureFromCallableToFirstClassCallableRector branch from ae9a5de to 8511042 Compare July 22, 2025 14:56
@samsonasik
Copy link
Member

Okay, let's give it a try, thank you @devnix

@samsonasik samsonasik merged commit eb07bff into rectorphp:main Jul 22, 2025
47 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Is there an upgrade rule from Closure::fromCallable() to first class callable syntax?
3 participants