55use PhpParser \Node ;
66use PhpParser \Node \Arg ;
77use PhpParser \Node \Expr ;
8+ use PhpParser \Node \Expr \ArrayDimFetch ;
89use PhpParser \Node \Expr \Assign ;
10+ use PhpParser \Node \Expr \BinaryOp \BooleanAnd ;
11+ use PhpParser \Node \Expr \BinaryOp \BooleanOr ;
912use PhpParser \Node \Expr \BooleanNot ;
1013use PhpParser \Node \Expr \FuncCall ;
14+ use PhpParser \Node \Expr \MethodCall ;
15+ use PhpParser \Node \Expr \PropertyFetch ;
16+ use PhpParser \Node \Expr \StaticCall ;
17+ use PhpParser \Node \Expr \StaticPropertyFetch ;
1118use PhpParser \Node \Expr \Throw_ ;
1219use PhpParser \Node \Expr \Variable ;
1320use PhpParser \Node \Name ;
1623use PhpParser \Node \Stmt \If_ ;
1724use PhpParser \NodeVisitor ;
1825use Rector \NodeTypeResolver \Node \AttributeKey ;
26+ use Rector \PhpParser \Node \BetterNodeFinder ;
1927use RectorLaravel \AbstractRector ;
2028use Symplify \RuleDocGenerator \ValueObject \CodeSample \CodeSample ;
2129use Symplify \RuleDocGenerator \ValueObject \RuleDefinition ;
2533 */
2634class ThrowIfRector extends AbstractRector
2735{
36+ public function __construct (private readonly BetterNodeFinder $ betterNodeFinder ) {}
37+
2838 public function getRuleDefinition (): RuleDefinition
2939 {
3040 return new RuleDefinition ('Change if throw to throw_if ' , [
@@ -46,51 +56,45 @@ public function getRuleDefinition(): RuleDefinition
4656 ]);
4757 }
4858
59+ /**
60+ * @return array<class-string<Node>>
61+ */
4962 public function getNodeTypes (): array
5063 {
5164 return [If_::class];
5265 }
5366
67+ /**
68+ * @param If_ $node
69+ */
5470 public function refactor (Node $ node ): ?Node
5571 {
56- if (! $ node instanceof If_) {
57- return null ;
58- }
59-
72+ /**
73+ * This is too conservative, can work with cases with elseif and else execution branches.
74+ * Skip for now for simplicity.
75+ */
6076 if ($ node ->else instanceof Else_ || $ node ->elseifs !== []) {
6177 return null ;
6278 }
6379
6480 $ ifStmts = $ node ->stmts ;
65-
66- // Check if there's a single throw statement inside the if
67- if (count ($ ifStmts ) !== 1 || ! $ ifStmts [0 ] instanceof Expression || ! $ ifStmts [0 ]->expr instanceof Throw_) {
81+ // Check if there's a single throw expression inside the if-statement
82+ if (! (count ($ ifStmts ) === 1 && $ ifStmts [0 ] instanceof Expression && $ ifStmts [0 ]->expr instanceof Throw_)) {
6883 return null ;
6984 }
7085
71- $ condition = $ node ->cond ;
86+ $ ifCondition = $ node ->cond ;
7287 $ throwExpr = $ ifStmts [0 ]->expr ;
7388
74- if ($ this ->exceptionUsesVariablesAssignedByCondition ($ throwExpr , $ condition )) {
89+ if (! $ this ->isSafeToTransform ($ throwExpr , $ ifCondition )) {
7590 return null ;
7691 }
7792
78- // Check if the condition is a negation
79- if ($ condition instanceof BooleanNot) {
80- // Create a new throw_unless function call
81- $ funcCall = new FuncCall (new Name ('throw_unless ' ), [
82- new Arg ($ condition ->expr ),
83- new Arg ($ throwExpr ->expr ),
84- ]);
85- } else {
86- // Create a new throw_if function call
87- $ funcCall = new FuncCall (new Name ('throw_if ' ), [
88- new Arg ($ condition ),
89- new Arg ($ throwExpr ->expr ),
90- ]);
91- }
92-
93- $ expression = new Expression ($ funcCall );
93+ $ expression = new Expression (
94+ $ ifCondition instanceof BooleanNot
95+ ? new FuncCall (new Name ('throw_unless ' ), [new Arg ($ ifCondition ->expr ), new Arg ($ throwExpr ->expr )])
96+ : new FuncCall (new Name ('throw_if ' ), [new Arg ($ ifCondition ), new Arg ($ throwExpr ->expr )])
97+ );
9498
9599 $ comments = array_merge ($ node ->getComments (), $ ifStmts [0 ]->getComments ());
96100 if ($ comments !== []) {
@@ -100,33 +104,50 @@ public function refactor(Node $node): ?Node
100104 return $ expression ;
101105 }
102106
103- /**
104- * Make sure the exception doesn't use variables assigned by the condition or this
105- * will cause broken code to be generated
106- */
107- private function exceptionUsesVariablesAssignedByCondition (Expr $ throwExpr , Expr $ condition ): bool
107+ private function isSafeToTransform (Throw_ $ throw , Expr $ expr ): bool
108108 {
109- $ conditionVariables = [];
110- $ returnValue = false ;
109+ $ shouldTransform = true ;
110+ $ bannedNodeTypes = [MethodCall::class, StaticCall::class, FuncCall::class, ArrayDimFetch::class, PropertyFetch::class, StaticPropertyFetch::class];
111+ $ this ->traverseNodesWithCallable ($ throw ->expr , function (Node $ node ) use (&$ shouldTransform , $ bannedNodeTypes , $ expr ): ?int {
112+ if (
113+ in_array ($ node ::class, $ bannedNodeTypes , true )
114+ || $ node instanceof Variable && ! $ this ->isSafeToTransformWithVariableAccess ($ node , $ expr )
115+ ) {
116+ $ shouldTransform = false ;
111117
112- $ this ->traverseNodesWithCallable ($ condition , function (Node $ node ) use (&$ conditionVariables ): null {
113- if ($ node instanceof Assign) {
114- $ conditionVariables [] = $ this ->getName ($ node ->var );
118+ return NodeVisitor::STOP_TRAVERSAL ;
115119 }
116120
117121 return null ;
118122 });
119123
120- $ this ->traverseNodesWithCallable ($ throwExpr , function (Node $ node ) use ($ conditionVariables , &$ returnValue ): ?int {
121- if ($ node instanceof Variable && in_array ($ this ->getName ($ node ), $ conditionVariables , true )) {
122- $ returnValue = true ;
124+ return $ shouldTransform ;
125+ }
123126
124- return NodeVisitor::STOP_TRAVERSAL ;
125- }
127+ /**
128+ * Not safe to transform when throw expression contains variables that are assigned in the if-condition because of the short-circuit logical operators issue.
129+ * This method checks if the variable was assigned on the right side of a short-circuit logical operator (conjunction and disjunction).
130+ * Note: The check is a little too strict, because such a variable may be initialized before the if-statement, and in such case it doesn't matter if it was assigned somewhere in the condition.
131+ */
132+ private function isSafeToTransformWithVariableAccess (Variable $ variable , Expr $ expr ): bool
133+ {
134+ $ firstShortCircuitOperator = $ this ->betterNodeFinder ->findFirst (
135+ $ expr ,
136+ fn (Node $ node ): bool => $ node instanceof BooleanAnd || $ node instanceof BooleanOr
137+ );
138+ if (! $ firstShortCircuitOperator instanceof Node) {
139+ return true ;
140+ }
141+ assert ($ firstShortCircuitOperator instanceof BooleanAnd || $ firstShortCircuitOperator instanceof BooleanOr);
126142
127- return null ;
128- });
143+ $ varName = $ this ->getName ($ variable );
144+ $ hasUnsafeAssignment = $ this ->betterNodeFinder ->findFirst (
145+ $ firstShortCircuitOperator ->right , // only here short-circuit problem can happen
146+ fn (Node $ node ): bool => $ node instanceof Assign
147+ && $ node ->var instanceof Variable
148+ && $ this ->getName ($ node ->var ) === $ varName
149+ );
129150
130- return $ returnValue ;
151+ return ! $ hasUnsafeAssignment instanceof Node ;
131152 }
132153}
0 commit comments