All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openrewrite.staticanalysis.TernaryOperatorsShouldNotBeNested Maven / Gradle / Ivy

/*
 * Copyright 2022 the original author or authors.
 * 

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.openrewrite.staticanalysis; import org.jspecify.annotations.Nullable; import org.openrewrite.ExecutionContext; import org.openrewrite.Recipe; import org.openrewrite.Tree; import org.openrewrite.TreeVisitor; import org.openrewrite.java.JavaIsoVisitor; import org.openrewrite.java.JavaVisitor; import org.openrewrite.java.marker.JavaVersion; import org.openrewrite.java.tree.*; import org.openrewrite.marker.Markers; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.openrewrite.Tree.randomId; import static org.openrewrite.java.tree.J.Binary.Type.Equal; public class TernaryOperatorsShouldNotBeNested extends Recipe { @Override public String getDisplayName() { return "Ternary operators should not be nested"; } @Override public String getDescription() { return "Nested ternary operators can be hard to read quickly. Prefer simpler constructs for improved readability. " + "If supported, this recipe will try to replace nested ternaries with switch expressions."; } @Override public Set getTags() { return Collections.singleton("RSPEC-S3358"); } @Override public TreeVisitor getVisitor() { return new JavaIsoVisitor() { @Override public J.CompilationUnit visitCompilationUnit( final J.CompilationUnit cu, final ExecutionContext ctx ) { if (cu.getMarkers() .findFirst(JavaVersion.class) .filter(javaVersion -> javaVersion.getMajorVersion() >= 14) .isPresent()) { doAfterVisit(new UseSwitchExpressionVisitor()); } doAfterVisit(new UseIfVisitor()); return cu; } }; } private static class UseIfVisitor extends JavaVisitor { @Override public J visitLambda(final J.Lambda lambda, final ExecutionContext ctx) { J result = rewriteNestedTernary(lambda); if (result == lambda) { return super.visitLambda(lambda, ctx); } doAfterVisit(new RemoveUnneededBlock().getVisitor()); return autoFormat(lambda.withBody(result.withPrefix(Space.SINGLE_SPACE)), ctx); } @Override public J visitReturn(final J.Return retrn, final ExecutionContext ctx) { J result = rewriteNestedTernary(retrn); if (result == retrn) { return super.visitReturn(retrn, ctx); } doAfterVisit(new RemoveUnneededBlock().getVisitor()); return autoFormat(result, ctx); } private Statement rewriteNestedTernary(final Statement parent) { return findTernary(parent).map(ternary -> { if (!isNestedTernary(ternary)) { return parent; } J.If iff = ifOf(ternary); J.Return otherwise = returnOf(ternary.getFalsePart()); return blockOf(iff, rewriteNestedTernary(otherwise)).withPrefix(parent.getPrefix()); }).orElse(parent); } private J.If ifOf(final J.Ternary ternary) { return new J.If( Tree.randomId(), ternary.getPrefix(), Markers.EMPTY, new J.ControlParentheses<>(Tree.randomId(), Space.EMPTY, Markers.EMPTY, JRightPadded.build(ternary.getCondition()) ).withComments(ternary.getCondition().getComments()), JRightPadded.build(blockOf(rewriteNestedTernary(returnOf(ternary.getTruePart() .withComments(ternary.getTruePart().getComments()))))), null ); } private static boolean isNestedTernary(final J possibleTernary) { int result = determineNestingLevels(possibleTernary, 0); return result > 1; } private static int determineNestingLevels(final J possibleTernary, final int level) { if (!(possibleTernary instanceof J.Ternary)) { return level; } J.Ternary ternary = (J.Ternary) possibleTernary; int truePath = determineNestingLevels(ternary.getTruePart(), level + 1); int falsePath = determineNestingLevels(ternary.getFalsePart(), level + 1); return Math.max(falsePath, truePath); } private static Optional findTernary(Statement parent) { J possibleTernary = parent; if (parent instanceof J.Return) { possibleTernary = ((J.Return) parent).getExpression(); } else if (parent instanceof J.Lambda) { possibleTernary = ((J.Lambda) parent).getBody(); } if (possibleTernary instanceof J.Ternary) { return Optional.of(possibleTernary).map(J.Ternary.class::cast); } return Optional.empty(); } } static class UseSwitchExpressionVisitor extends JavaVisitor { @Override public J visitTernary(final J.Ternary ternary, final ExecutionContext ctx) { return findConditionIdentifier(ternary).map(switchVar -> { List nestList = findNestedTernaries(ternary, switchVar); if (nestList.size() < 2) { return null; } return autoFormat(toSwitch(switchVar, nestList), ctx); }).map(J.class::cast) .orElseGet(() -> super.visitTernary(ternary, ctx)); } private List findNestedTernaries(final J.Ternary ternary, final J.Identifier switchVar) { List nestList = new ArrayList<>(); J.Ternary next = ternary; while (next.getFalsePart() instanceof J.Ternary) { if (next.getTruePart() instanceof J.Ternary) { //as long as we do not use pattern matching, an "and" nested ternary will never work for a switch: // Example: a equals a and a equals b will never be true return Collections.emptyList(); } J.Ternary nested = (J.Ternary) next.getFalsePart(); if (!findConditionIdentifier(nested) .filter(found -> isEqualVariable(switchVar, found)) .isPresent()) { return Collections.emptyList(); } nestList.add(next); next = nested; } nestList.add(next); return nestList; } private static boolean isEqualVariable(final J.Identifier switchVar, @Nullable final J found) { if (!(found instanceof J.Identifier)) { return false; } J.Identifier foundVar = (J.Identifier) found; return Objects.equals(foundVar.getFieldType(), switchVar.getFieldType()); } private J.SwitchExpression toSwitch(final J.Identifier switchVar, final List nestList) { J.Ternary last = nestList.get(nestList.size() - 1); return new J.SwitchExpression( Tree.randomId(), Space.SINGLE_SPACE, Markers.EMPTY, new J.ControlParentheses<>( Tree.randomId(), switchVar.getPrefix().withWhitespace(" "), switchVar.getMarkers(), JRightPadded.build(switchVar.withPrefix(Space.EMPTY)) ), blockOf(Stream.concat( nestList.stream().map(ternary -> toCase(switchVar, ternary)), Stream.of(toDefault(last)) ).collect(Collectors.toList())) .withPrefix(Space.SINGLE_SPACE) ); } private J.Case toCase(final J.Identifier switchVar, final J.Ternary ternary) { Expression compare; if (ternary.getCondition() instanceof J.MethodInvocation) { J.MethodInvocation inv = ((J.MethodInvocation) ternary.getCondition()); if (isObjectsEquals(inv)) { maybeRemoveImport("java.util.Objects"); compare = isVariable(inv.getArguments().get(0)) ? inv.getArguments().get(1) : inv.getArguments().get(0); } else { compare = isEqualVariable(switchVar, inv.getSelect()) ? inv.getArguments().get(0) : inv.getSelect(); } } else if (isEqualsBinary(ternary.getCondition())) { J.Binary bin = ((J.Binary) ternary.getCondition()); compare = isEqualVariable(switchVar, bin.getLeft()) ? bin.getRight() : bin.getLeft(); } else { throw new IllegalArgumentException( "Only J.Binary or J.MethodInvocation are expected as ternary conditions when creating a switch case"); } return new J.Case( Tree.randomId(), ternary.getPrefix().withWhitespace(" "), ternary.getMarkers(), J.Case.Type.Rule, JContainer.build( Collections.singletonList(JRightPadded.build(compare.withPrefix(Space.SINGLE_SPACE)) .withAfter(Space.SINGLE_SPACE)) ), JContainer.build(Collections.emptyList()), JRightPadded.build(ternary.getTruePart()) ); } private J.Case toDefault(final J.Ternary ternary) { return new J.Case( Tree.randomId(), Space.EMPTY, ternary.getMarkers(), J.Case.Type.Rule, JContainer.build(Collections.singletonList(JRightPadded.build(new J.Identifier( randomId(), Space.EMPTY, Markers.EMPTY, "default", null, null )).withAfter(Space.SINGLE_SPACE))), JContainer.build(Collections.emptyList()), JRightPadded.build(ternary.getFalsePart()) ); } private Optional findConditionIdentifier(final J.Ternary ternary) { J.Identifier result = null; if (ternary.getCondition() instanceof J.MethodInvocation) { J.MethodInvocation inv = (J.MethodInvocation) ternary.getCondition(); if (!inv.getSimpleName().equals("equals")) { return Optional.empty(); } if (inv.getArguments().size() == 1) { J other = null; if (isVariable(inv.getSelect())) { result = (J.Identifier) inv.getSelect(); other = inv.getArguments().get(0); } if (inv.getArguments().get(0) instanceof J.Identifier) { result = (J.Identifier) inv.getArguments().get(0); other = inv.getSelect(); } if (!isConstant(other)) { return Optional.empty(); } } } else if (isEqualsBinary(ternary.getCondition())) { J.Binary bin = (J.Binary) ternary.getCondition(); result = xorVariable(bin.getLeft(), bin.getRight()); } return Optional.ofNullable(result); } private static J.@Nullable Identifier xorVariable(J first, J second) { J.Identifier result = null; if (isVariable(first) && isVariable(second)) { return null; } if (isVariable(first)) { result = (J.Identifier) first; } if (isVariable(second)) { result = (J.Identifier) second; } return result; } private static boolean isVariable(@Nullable J maybeVariable) { if (maybeVariable == null) { return false; } if (!(maybeVariable instanceof J.Identifier)) { return false; } J.Identifier identifier = (J.Identifier) maybeVariable; if (identifier.getFieldType() == null) { return false; } return !identifier.getFieldType().hasFlags(Flag.Final) || !identifier.getFieldType().hasFlags(Flag.Static); } private static boolean isConstant(@Nullable J maybeConstant) { if (maybeConstant == null) { return false; } if (maybeConstant instanceof J.Literal) { return true; } if (!(maybeConstant instanceof J.Identifier)) { return false; } J.Identifier identifier = (J.Identifier) maybeConstant; if (identifier.getFieldType() == null) { return false; } return !identifier.getFieldType().hasFlags(Flag.Final) || !identifier.getFieldType().hasFlags(Flag.Static); } private static boolean isObjectsEquals(J.MethodInvocation inv) { if (inv.getSelect() instanceof J.Identifier) { J.Identifier maybeObjects = (J.Identifier) inv.getSelect(); boolean isObjects = TypeUtils.isOfClassType(maybeObjects.getType(), "java.util.Objects"); return isObjects && "equals".equals(inv.getSimpleName()); } return false; } private static boolean isEqualsBinary(J maybeEqualsBinary) { return maybeEqualsBinary instanceof J.Binary && ((J.Binary) maybeEqualsBinary).getOperator().equals(Equal); } } private static J.Return returnOf(Expression expression) { return new J.Return(Tree.randomId(), Space.EMPTY, Markers.EMPTY, expression.withPrefix(Space.EMPTY)) .withComments(expression.getComments()); } private static J.Block blockOf(Statement... statements) { return blockOf(Arrays.asList(statements)); } private static J.Block blockOf(List statements) { return J.Block.createEmptyBlock().withStatements(statements); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy