44namespace Rector \Php70 \Rector \Ternary ;
55
66use PhpParser \Node ;
7+ use PhpParser \Node \Arg ;
78use PhpParser \Node \Expr ;
89use PhpParser \Node \Expr \BinaryOp ;
910use PhpParser \Node \Expr \BinaryOp \Coalesce ;
1011use PhpParser \Node \Expr \BinaryOp \Identical ;
1112use PhpParser \Node \Expr \BinaryOp \NotIdentical ;
13+ use PhpParser \Node \Expr \BooleanNot ;
14+ use PhpParser \Node \Expr \FuncCall ;
1215use PhpParser \Node \Expr \Isset_ ;
1316use PhpParser \Node \Expr \Ternary ;
1417use Rector \NodeTypeResolver \Node \AttributeKey ;
@@ -34,7 +37,7 @@ public function __construct(ValueResolver $valueResolver)
3437 }
3538 public function getRuleDefinition (): RuleDefinition
3639 {
37- return new RuleDefinition ('Changes unneeded null check to ?? operator ' , [new CodeSample ('$value === null ? 10 : $value; ' , '$value ?? 10; ' ), new CodeSample ('isset($value) ? $value : 10; ' , '$value ?? 10; ' )]);
40+ return new RuleDefinition ('Changes unneeded null check to ?? operator ' , [new CodeSample ('$value === null ? 10 : $value; ' , '$value ?? 10; ' ), new CodeSample ('isset($value) ? $value : 10; ' , '$value ?? 10; ' ), new CodeSample ( ' is_null($value) ? 10 : $value; ' , ' $value ?? 10; ' ) ]);
3841 }
3942 /**
4043 * @return array<class-string<Node>>
@@ -51,6 +54,12 @@ public function refactor(Node $node): ?Node
5154 if ($ node ->cond instanceof Isset_) {
5255 return $ this ->processTernaryWithIsset ($ node , $ node ->cond );
5356 }
57+ if ($ node ->cond instanceof FuncCall && $ this ->isName ($ node ->cond , 'is_null ' )) {
58+ return $ this ->processTernaryWithIsNull ($ node , $ node ->cond , \false);
59+ }
60+ if ($ node ->cond instanceof BooleanNot && $ node ->cond ->expr instanceof FuncCall && $ this ->isName ($ node ->cond ->expr , 'is_null ' )) {
61+ return $ this ->processTernaryWithIsNull ($ node , $ node ->cond ->expr , \true);
62+ }
5463 if ($ node ->cond instanceof Identical) {
5564 $ checkedNode = $ node ->else ;
5665 $ fallbackNode = $ node ->if ;
@@ -69,17 +78,46 @@ public function refactor(Node $node): ?Node
6978 }
7079 $ ternaryCompareNode = $ node ->cond ;
7180 if ($ this ->isNullMatch ($ ternaryCompareNode ->left , $ ternaryCompareNode ->right , $ checkedNode )) {
72- return new Coalesce ($ checkedNode , $ fallbackNode );
81+ return $ this -> createCoalesce ($ checkedNode , $ fallbackNode );
7382 }
7483 if ($ this ->isNullMatch ($ ternaryCompareNode ->right , $ ternaryCompareNode ->left , $ checkedNode )) {
75- return new Coalesce ($ checkedNode , $ fallbackNode );
84+ return $ this -> createCoalesce ($ checkedNode , $ fallbackNode );
7685 }
7786 return null ;
7887 }
7988 public function provideMinPhpVersion (): int
8089 {
8190 return PhpVersionFeature::NULL_COALESCE ;
8291 }
92+ private function processTernaryWithIsNull (Ternary $ ternary , FuncCall $ isNullFuncCall , bool $ isNegated ): ?Coalesce
93+ {
94+ if (count ($ isNullFuncCall ->args ) !== 1 ) {
95+ return null ;
96+ }
97+ $ firstArg = $ isNullFuncCall ->args [0 ];
98+ if (!$ firstArg instanceof Arg) {
99+ return null ;
100+ }
101+ $ checkedExpr = $ firstArg ->value ;
102+ if ($ isNegated ) {
103+ if (!$ ternary ->if instanceof Expr) {
104+ return null ;
105+ }
106+ if (!$ this ->nodeComparator ->areNodesEqual ($ ternary ->if , $ checkedExpr )) {
107+ return null ;
108+ }
109+ $ this ->preserveWrappedFallback ($ ternary ->else );
110+ return $ this ->createCoalesce ($ ternary ->if , $ ternary ->else );
111+ }
112+ if (!$ this ->nodeComparator ->areNodesEqual ($ ternary ->else , $ checkedExpr )) {
113+ return null ;
114+ }
115+ if (!$ ternary ->if instanceof Expr) {
116+ return null ;
117+ }
118+ $ this ->preserveWrappedFallback ($ ternary ->if );
119+ return $ this ->createCoalesce ($ ternary ->else , $ ternary ->if );
120+ }
83121 private function processTernaryWithIsset (Ternary $ ternary , Isset_ $ isset ): ?Coalesce
84122 {
85123 if (!$ ternary ->if instanceof Expr) {
@@ -98,7 +136,7 @@ private function processTernaryWithIsset(Ternary $ternary, Isset_ $isset): ?Coal
98136 if (($ ternary ->else instanceof Ternary || $ ternary ->else instanceof BinaryOp) && $ this ->isTernaryParenthesized ($ this ->getFile (), $ ternary ->cond , $ ternary )) {
99137 $ ternary ->else ->setAttribute (AttributeKey::WRAPPED_IN_PARENTHESES , \true);
100138 }
101- return new Coalesce ($ ternary ->if , $ ternary ->else );
139+ return $ this -> createCoalesce ($ ternary ->if , $ ternary ->else );
102140 }
103141 private function isTernaryParenthesized (File $ file , Expr $ expr , Ternary $ ternary ): bool
104142 {
@@ -129,4 +167,34 @@ private function isNullMatch(Expr $possibleNullExpr, Expr $firstNode, Expr $seco
129167 }
130168 return $ this ->nodeComparator ->areNodesEqual ($ firstNode , $ secondNode );
131169 }
170+ private function createCoalesce (Expr $ checkedExpr , Expr $ fallbackExpr ): Coalesce
171+ {
172+ if ($ this ->isExprParenthesized ($ this ->getFile (), $ checkedExpr )) {
173+ $ checkedExpr ->setAttribute (AttributeKey::WRAPPED_IN_PARENTHESES , \true);
174+ }
175+ return new Coalesce ($ checkedExpr , $ fallbackExpr );
176+ }
177+ private function preserveWrappedFallback (Expr $ expr ): void
178+ {
179+ if (!$ expr instanceof BinaryOp && !$ expr instanceof Ternary) {
180+ return ;
181+ }
182+ if (!$ this ->isExprParenthesized ($ this ->getFile (), $ expr )) {
183+ return ;
184+ }
185+ $ expr ->setAttribute (AttributeKey::WRAPPED_IN_PARENTHESES , \true);
186+ }
187+ private function isExprParenthesized (File $ file , Expr $ expr ): bool
188+ {
189+ $ oldTokens = $ file ->getOldTokens ();
190+ $ startTokenPos = $ expr ->getStartTokenPos ();
191+ $ endTokenPos = $ expr ->getEndTokenPos ();
192+ while (isset ($ oldTokens [$ startTokenPos - 1 ]) && trim ((string ) $ oldTokens [$ startTokenPos - 1 ]) === '' ) {
193+ --$ startTokenPos ;
194+ }
195+ while (isset ($ oldTokens [$ endTokenPos + 1 ]) && trim ((string ) $ oldTokens [$ endTokenPos + 1 ]) === '' ) {
196+ ++$ endTokenPos ;
197+ }
198+ return isset ($ oldTokens [$ startTokenPos - 1 ], $ oldTokens [$ endTokenPos + 1 ]) && (string ) $ oldTokens [$ startTokenPos - 1 ] === '( ' && (string ) $ oldTokens [$ endTokenPos + 1 ] === ') ' ;
199+ }
132200}
0 commit comments