55namespace Rector \Php70 \Rector \Ternary ;
66
77use PhpParser \Node ;
8+ use PhpParser \Node \Arg ;
89use PhpParser \Node \Expr ;
910use PhpParser \Node \Expr \BinaryOp ;
1011use PhpParser \Node \Expr \BinaryOp \Coalesce ;
1112use PhpParser \Node \Expr \BinaryOp \Identical ;
1213use PhpParser \Node \Expr \BinaryOp \NotIdentical ;
14+ use PhpParser \Node \Expr \BooleanNot ;
15+ use PhpParser \Node \Expr \FuncCall ;
1316use PhpParser \Node \Expr \Isset_ ;
1417use PhpParser \Node \Expr \Ternary ;
1518use Rector \NodeTypeResolver \Node \AttributeKey ;
@@ -38,6 +41,7 @@ public function getRuleDefinition(): RuleDefinition
3841 [
3942 new CodeSample ('$value === null ? 10 : $value; ' , '$value ?? 10; ' ),
4043 new CodeSample ('isset($value) ? $value : 10; ' , '$value ?? 10; ' ),
44+ new CodeSample ('is_null($value) ? 10 : $value; ' , '$value ?? 10; ' ),
4145 ]
4246 );
4347 }
@@ -59,6 +63,17 @@ public function refactor(Node $node): ?Node
5963 return $ this ->processTernaryWithIsset ($ node , $ node ->cond );
6064 }
6165
66+ if ($ node ->cond instanceof FuncCall && $ this ->isName ($ node ->cond , 'is_null ' )) {
67+ return $ this ->processTernaryWithIsNull ($ node , $ node ->cond , false );
68+ }
69+
70+ if (
71+ $ node ->cond instanceof BooleanNot && $ node ->cond ->expr instanceof FuncCall
72+ && $ this ->isName ($ node ->cond ->expr , 'is_null ' )
73+ ) {
74+ return $ this ->processTernaryWithIsNull ($ node , $ node ->cond ->expr , true );
75+ }
76+
6277 if ($ node ->cond instanceof Identical) {
6378 $ checkedNode = $ node ->else ;
6479 $ fallbackNode = $ node ->if ;
@@ -80,11 +95,11 @@ public function refactor(Node $node): ?Node
8095
8196 $ ternaryCompareNode = $ node ->cond ;
8297 if ($ this ->isNullMatch ($ ternaryCompareNode ->left , $ ternaryCompareNode ->right , $ checkedNode )) {
83- return new Coalesce ($ checkedNode , $ fallbackNode );
98+ return $ this -> createCoalesce ($ checkedNode , $ fallbackNode );
8499 }
85100
86101 if ($ this ->isNullMatch ($ ternaryCompareNode ->right , $ ternaryCompareNode ->left , $ checkedNode )) {
87- return new Coalesce ($ checkedNode , $ fallbackNode );
102+ return $ this -> createCoalesce ($ checkedNode , $ fallbackNode );
88103 }
89104
90105 return null ;
@@ -95,6 +110,46 @@ public function provideMinPhpVersion(): int
95110 return PhpVersionFeature::NULL_COALESCE ;
96111 }
97112
113+ private function processTernaryWithIsNull (Ternary $ ternary , FuncCall $ isNullFuncCall , bool $ isNegated ): ?Coalesce
114+ {
115+ if (count ($ isNullFuncCall ->args ) !== 1 ) {
116+ return null ;
117+ }
118+
119+ $ firstArg = $ isNullFuncCall ->args [0 ];
120+ if (! $ firstArg instanceof Arg) {
121+ return null ;
122+ }
123+
124+ $ checkedExpr = $ firstArg ->value ;
125+
126+ if ($ isNegated ) {
127+ if (! $ ternary ->if instanceof Expr) {
128+ return null ;
129+ }
130+
131+ if (! $ this ->nodeComparator ->areNodesEqual ($ ternary ->if , $ checkedExpr )) {
132+ return null ;
133+ }
134+
135+ $ this ->preserveWrappedFallback ($ ternary ->else );
136+
137+ return $ this ->createCoalesce ($ ternary ->if , $ ternary ->else );
138+ }
139+
140+ if (! $ this ->nodeComparator ->areNodesEqual ($ ternary ->else , $ checkedExpr )) {
141+ return null ;
142+ }
143+
144+ if (! $ ternary ->if instanceof Expr) {
145+ return null ;
146+ }
147+
148+ $ this ->preserveWrappedFallback ($ ternary ->if );
149+
150+ return $ this ->createCoalesce ($ ternary ->else , $ ternary ->if );
151+ }
152+
98153 private function processTernaryWithIsset (Ternary $ ternary , Isset_ $ isset ): ?Coalesce
99154 {
100155 if (! $ ternary ->if instanceof Expr) {
@@ -122,7 +177,7 @@ private function processTernaryWithIsset(Ternary $ternary, Isset_ $isset): ?Coal
122177 $ ternary ->else ->setAttribute (AttributeKey::WRAPPED_IN_PARENTHESES , true );
123178 }
124179
125- return new Coalesce ($ ternary ->if , $ ternary ->else );
180+ return $ this -> createCoalesce ($ ternary ->if , $ ternary ->else );
126181 }
127182
128183 private function isTernaryParenthesized (File $ file , Expr $ expr , Ternary $ ternary ): bool
@@ -163,4 +218,45 @@ private function isNullMatch(Expr $possibleNullExpr, Expr $firstNode, Expr $seco
163218
164219 return $ this ->nodeComparator ->areNodesEqual ($ firstNode , $ secondNode );
165220 }
221+
222+ private function createCoalesce (Expr $ checkedExpr , Expr $ fallbackExpr ): Coalesce
223+ {
224+ if ($ this ->isExprParenthesized ($ this ->getFile (), $ checkedExpr )) {
225+ $ checkedExpr ->setAttribute (AttributeKey::WRAPPED_IN_PARENTHESES , true );
226+ }
227+
228+ return new Coalesce ($ checkedExpr , $ fallbackExpr );
229+ }
230+
231+ private function preserveWrappedFallback (Expr $ expr ): void
232+ {
233+ if (! $ expr instanceof BinaryOp && ! $ expr instanceof Ternary) {
234+ return ;
235+ }
236+
237+ if (! $ this ->isExprParenthesized ($ this ->getFile (), $ expr )) {
238+ return ;
239+ }
240+
241+ $ expr ->setAttribute (AttributeKey::WRAPPED_IN_PARENTHESES , true );
242+ }
243+
244+ private function isExprParenthesized (File $ file , Expr $ expr ): bool
245+ {
246+ $ oldTokens = $ file ->getOldTokens ();
247+ $ startTokenPos = $ expr ->getStartTokenPos ();
248+ $ endTokenPos = $ expr ->getEndTokenPos ();
249+
250+ while (isset ($ oldTokens [$ startTokenPos - 1 ]) && trim ((string ) $ oldTokens [$ startTokenPos - 1 ]) === '' ) {
251+ --$ startTokenPos ;
252+ }
253+
254+ while (isset ($ oldTokens [$ endTokenPos + 1 ]) && trim ((string ) $ oldTokens [$ endTokenPos + 1 ]) === '' ) {
255+ ++$ endTokenPos ;
256+ }
257+
258+ return isset ($ oldTokens [$ startTokenPos - 1 ], $ oldTokens [$ endTokenPos + 1 ])
259+ && (string ) $ oldTokens [$ startTokenPos - 1 ] === '( '
260+ && (string ) $ oldTokens [$ endTokenPos + 1 ] === ') ' ;
261+ }
166262}
0 commit comments