binaryOpManipulator = $binaryOpManipulator; $this->valueResolver = $valueResolver; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Simplify `foreach` loops into `in_array` when possible', [new CodeSample(<<<'CODE_SAMPLE' foreach ($items as $item) { if ($item === 'something') { return true; } } return false; CODE_SAMPLE , <<<'CODE_SAMPLE' return in_array('something', $items, true); CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [StmtsAwareInterface::class]; } /** * @param StmtsAwareInterface $node */ public function refactor(Node $node) : ?Node { if ($node->stmts === null) { return null; } foreach ($node->stmts as $key => $stmt) { if (!$stmt instanceof Return_) { continue; } $prevStmt = $node->stmts[$key - 1] ?? null; if (!$prevStmt instanceof Foreach_) { continue; } $return = $stmt; $foreach = $prevStmt; if ($this->shouldSkipForeach($foreach)) { return null; } /** @var If_ $firstNodeInsideForeach */ $firstNodeInsideForeach = $foreach->stmts[0]; if ($this->shouldSkipIf($firstNodeInsideForeach)) { return null; } /** @var Identical|Equal $ifCondition */ $ifCondition = $firstNodeInsideForeach->cond; $twoNodeMatch = $this->matchNodes($ifCondition, $foreach->valueVar); if (!$twoNodeMatch instanceof TwoNodeMatch) { return null; } $comparedExpr = $twoNodeMatch->getSecondExpr(); if (!$this->isIfBodyABoolReturnNode($firstNodeInsideForeach)) { return null; } $foreachReturn = $firstNodeInsideForeach->stmts[0]; if (!$foreachReturn instanceof Return_) { return null; } if (!$return->expr instanceof Expr) { return null; } if (!$this->valueResolver->isTrueOrFalse($return->expr)) { return null; } if (!$foreachReturn->expr instanceof Expr) { return null; } // cannot be "return true;" + "return true;" if ($this->nodeComparator->areNodesEqual($return, $foreachReturn)) { return null; } // 1. remove foreach unset($node->stmts[$key - 1]); // 2. make return of in_array() $funcCall = $this->createInArrayFunction($comparedExpr, $ifCondition, $foreach); $return = $this->createReturn($foreachReturn->expr, $funcCall); $node->stmts[$key] = $return; return $node; } return null; } private function shouldSkipForeach(Foreach_ $foreach) : bool { if ($foreach->keyVar instanceof Expr) { return \true; } if (\count($foreach->stmts) > 1) { return \true; } if (!$foreach->stmts[0] instanceof If_) { return \true; } $foreachValueStaticType = $this->getType($foreach->expr); return $foreachValueStaticType instanceof ObjectType; } private function shouldSkipIf(If_ $if) : bool { $ifCondition = $if->cond; if ($ifCondition instanceof Identical) { return \false; } return !$ifCondition instanceof Equal; } /** * @param \PhpParser\Node\Expr\BinaryOp\Equal|\PhpParser\Node\Expr\BinaryOp\Identical $binaryOp */ private function matchNodes($binaryOp, Expr $expr) : ?TwoNodeMatch { return $this->binaryOpManipulator->matchFirstAndSecondConditionNode($binaryOp, Variable::class, function (Node $node, Node $otherNode) use($expr) : bool { return $this->nodeComparator->areNodesEqual($otherNode, $expr); }); } private function isIfBodyABoolReturnNode(If_ $if) : bool { $ifStatment = $if->stmts[0]; if (!$ifStatment instanceof Return_) { return \false; } if (!$ifStatment->expr instanceof Expr) { return \false; } return $this->valueResolver->isTrueOrFalse($ifStatment->expr); } /** * @param \PhpParser\Node\Expr\BinaryOp\Identical|\PhpParser\Node\Expr\BinaryOp\Equal $binaryOp */ private function createInArrayFunction(Expr $expr, $binaryOp, Foreach_ $foreach) : FuncCall { $arguments = $this->nodeFactory->createArgs([$expr, $foreach->expr]); if ($binaryOp instanceof Identical) { $arguments[] = $this->nodeFactory->createArg($this->nodeFactory->createTrue()); } return $this->nodeFactory->createFuncCall('in_array', $arguments); } private function createReturn(Expr $expr, FuncCall $funcCall) : Return_ { $expr = $this->valueResolver->isFalse($expr) ? new BooleanNot($funcCall) : $funcCall; return new Return_($expr); } }