-
-
Notifications
You must be signed in to change notification settings - Fork 154
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
Change mutator API to return an object we control #494
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for contributing to Infection. We noticed you didn't add any tests. Could you please add them to make sure everything works as expected?
/** | ||
* @param Node|Node[] $node | ||
*/ | ||
public function __construct($node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't have a typehint, as a 'single' mutation can also return an array of nodes to replace a node.
I could also split it up in an ArrayMutationResult
or something like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would do it. With this PR, we want to avoid mixed
implicit typehint, so why to leave it here. Let it be Node
and array|Node[]
explicitly
Weird, i did add some tests, and the bot didn't pick up on it. |
64363b6
to
3965336
Compare
/** | ||
* @internal | ||
*/ | ||
interface MutatorResult |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My 5 cents:
What about to call an Interface like MutatedNode
and create the same class. Because MutatorResult
is smth abstract and doesn't reflect what is it and what for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also agree that MutatorResult
is too common name, but MutatedNode
is also confusing when \Generator
is used, even when Node[]
is used (because it's not singular but plural, while MutatedNode
is strictly singular)
What if we rename it in a way as it is a strategy, not the result of something:
interface NodeReplacer {
public function replaceWithNodes(): \Generator; // or getReplacedNodes(): \Generator
}
For a single Node
class SingleNodeReplacer implements NodeReplacer {
public function __construct(Node $node) { ... }
public function replaceWithNodes(): \Generator {
yield $this->node;
}
}
for array of Nodes
class ArrayNodeReplacer implements NodeReplacer {
public function __construct(Node[] $node) { ... }
public function replaceWithNodes(): \Generator {
yield $this->nodes;
}
}
for Generator
class GeneratorNodeReplacer implements NodeReplacer {
public function __construct(\Generator $nodes) { ... }
public function replaceWithNodes(): \Generator {
return $this->nodes;
}
}
client code would be
...
$nodeReplacer = $mutator->mutate($node);
foreach ($nodeReplacer->replaceWithNodes() as $node) { ... }
// or
foreach ($nodeReplacer->getReplacedNodes() as $node) { ... }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably, the previous comment is too much.
Another ideas:
MutatedAst
as an interface name (which implies singular/plural)
Or, if we continue PHP-Parser wording (see NodeTraverser
), we can name it NodeReplacement
:
abstract public function mutate(Node $node): NodeReplacement;
This one also implies singular and plural.
SingleNodeReplacement implements NodeReplacement
ArrayNodeReplacement implements NodeReplacement
- ...
I will have a look. Seems like Github API is paginated, and the first page of so many updated files didn't have test file |
@@ -9,7 +9,7 @@ | |||
|
|||
namespace Infection\Exception; | |||
|
|||
use Infection\Mutator\Util\Mutator; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any reason behind it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like the logic behind this is that this is a major class/interface that deserves to be at the root of the similarly named namespace. Just like a Mutation
.
Now I wonder if other classes inside Mutator\Util
deserve a similar treatment. Maybe moving them all one directory level down would be a better bet? As other directories there correspond to different classes of mutators (say, @cast
or @boolean
), Util
seems like a bit off. What do y'all think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deserves to be at the root of the similarly named namespace
sounds reasonable
Maybe moving them all one directory level down would be a better bet?
I'm fine with it, the only one note here is to update these lines
infection/tests/Mutator/Util/MutatorProfileTest.php
Lines 42 to 43 in ef45b8e
->in('src/Mutator') | |
->exclude('Util') |
probably, it's out of the scope of this PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This proposal left me baffled. Yeah, it's kind of cool to add these new contracts and interfaces, but other than that... Aren't we about to add more complexity to already very complex project for no particular reason? There must be some rationale for this change, for sanity's sake. What problem is this supposed to solve? What are cons and benefits?
For example, right now adding a mutator is a breeze. With this change that will be ever so slightly more difficult, because one would have to learn two or three more things more than they used to learn to just add a new mutator. Kind of bummer, don't you think?
OK, if you would let me articulate, the purpose of this change is to make things understandable from the first sight. Right now we have a kind of funny of a return declaration which mixes a generator with a node, @return \Generator|Node[]
, which is, well, not a bad thing if you remember what that means. A year later you won't necessarily so.
So it's either MutatorResult
, or a paragraph with an explanation somewhere. The first is better because it's going to be literally everywhere, like jumping in your face. Unlike the other.
*/ | ||
public function mutate(Node $node): MutatorResult | ||
{ | ||
return new GeneratorResult($this->getMutatedNodes($node)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding an extra function just for a generator is a bummer. This could be like:
return new GeneratorResult(static function () use ($node) {
yield $stuff;
});
And then GeneratorResult
could handle calling this function as needed or even right away.
{ | ||
return new Node\Expr\BinaryOp\Identical($node->left, $node->right, $node->getAttributes()); | ||
return new Mutator\Util\Result\SingleMutationResult(new Node\Expr\BinaryOp\Identical($node->left, $node->right, $node->getAttributes())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Long name here, short name in other places?..
/** | ||
* @return \Generator|Node[] | ||
*/ | ||
public function getResults(): \Generator; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not iterable
?
public function getResults(): \Generator | ||
{ | ||
yield $this->node; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With an iterable
typehint this could return a simple array.
this is a price for avoiding Also, I doubt that anyone "used to learn" anything about mutators. Mutators are closed at the moment, we don't expose public interfaces to write custom mutators, and 90%+ of mutators are written by core team. So for the majority of new developers this will be just new thing, no need to re-learn.
We can create abstract classes to return that simplicity back again, example for the abstract class SingleMutator extends Mutator
{
abstract protected function getMutatedNode(Node $node): Node;
public function mutate(Node $node): NodeReplacement
{
return SingleNodeReplacement($this->getMutatedNode($node);
}
} And then adding new mutator will be easy: final class Plus extends SingleMutator
{
/**
* Replaces "+" with "-"
*
* @param Node $node
*
* @return Node\Expr\BinaryOp\Minus
*/
protected function getMutatedNode(Node $node): Node
{
return new Node\Expr\BinaryOp\Minus($node->left, $node->right, $node->getAttributes());
}
protected function mutatesNode(Node $node): bool
{
// ...
}
} We will end up with 3 new abstract classes with clear goals, and the main method |
Yeah, that's kind of funny return declaration, I'd have a similar reaction to it if I wouldn't know what means what. I see the point, that's going to make things easier to maintain in the long-medium term. (Mutators are not closed at the moment in a sense there are folks who may be planning new mutators right about now, like you see with #500. Bringing simplicity back again will be most beneficial for them.) |
I wonder if we can move forward with this. |
1a0ed00
to
060fbed
Compare
@infection/core sorry about the delay on this PR, i've updated it to use 3 abstract mutator classes that can be used depending on what implementation is needed. |
0abc88f
to
c7eb237
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it, just one comment regarding visibility of the methods
*/ | ||
public function mutate(Node $node): MutatorResult | ||
public function getMutatedNode(Node $node): Node |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be protected
, not public
.
abstract protected function getMutatedNode(Node $node): array;
in the parent
Same for all classes.
1cefad4
to
3157333
Compare
src/Mutator/Util/SingleMutator.php
Outdated
|
||
final public function mutate(Node $node): NodeReplacement | ||
{ | ||
return new SingleNodeReplacement($this->getMutatedNode($node)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NodeReplacement
seems reductant to me.
If Mutator is defined as
public function mutate(Node $node): \Generator;
We can shortcut and remove all these result types, handling differences right inside mutator subclasses.
SingleMutator
will be like:
abstract protected function getMutatedNode(Node $node): Node;
final public function mutate(Node $node): \Generator
{
yield $this->getMutatedNode($node);
}
SingleMutator
and ArrayMutator
can share a superclass.
If not, ArrayMutator
will be like:
{
abstract protected function getMutatedNode(Node $node): array;
final public function mutate(Node $node): \Generator
{
yield $this->getMutatedNode($node);
}
}
GeneratorMutator
will be like
{
abstract protected function getMutatedNode(Node $node): iterable;
final public function mutate(Node $node): \Generator
{
yield from $this->getMutatedNode($node);
}
}
iterable
typehint gives us flexibility in a case where we would want to return a plain old array with mutations.
366ffeb
to
0e40c40
Compare
0e40c40
to
8f57f36
Compare
After some reconsideration i've decided to abandon this PR, it doesn't seem worth it. |
This PR:
MutatorResult
interface, which is returned by themutate
method.GeneratorResult
&SingleMutationResult
classes, which are implementations ofMutatorResult
Mutator
class the theInfection
namespace.InterfaceParentTrait
and moved the method into thePublicVisibility
classThis is the implementation of the idea mentioned in #450.