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

org.sonar.java.checks.BoxedBooleanExpressionsCheck Maven / Gradle / Ivy

There is a newer version: 8.10.0.38194
Show newest version
/*
 * SonarQube Java
 * Copyright (C) 2012-2024 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the Sonar Source-Available License for more details.
 *
 * You should have received a copy of the Sonar Source-Available License
 * along with this program; if not, see https://sonarsource.com/license/ssal/
 */
package org.sonar.java.checks;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.CheckForNull;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.QuickFixHelper;
import org.sonar.java.reporting.JavaQuickFix;
import org.sonar.java.reporting.JavaTextEdit;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
import org.sonar.plugins.java.api.semantic.Type.Primitives;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.BinaryExpressionTree;
import org.sonar.plugins.java.api.tree.ConditionalExpressionTree;
import org.sonar.plugins.java.api.tree.DoWhileStatementTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.ForStatementTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.IfStatementTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.Tree.Kind;
import org.sonar.plugins.java.api.tree.TypeCastTree;
import org.sonar.plugins.java.api.tree.UnaryExpressionTree;
import org.sonar.plugins.java.api.tree.WhileStatementTree;

import static org.sonar.plugins.java.api.semantic.MethodMatchers.ANY;

@Rule(key = "S5411")
public class BoxedBooleanExpressionsCheck extends BaseTreeVisitor implements JavaFileScanner {

  private static final String MESSAGE = "Use a primitive boolean expression here.";
  private static final String MESSAGE_QUICKFIX = "Use a primitive boolean expression";

  private static final MethodMatchers OPTIONAL_OR_ELSE = MethodMatchers.create()
    .ofTypes("java.util.Optional").names("orElse").addParametersMatcher(ANY).build();

  private static final String BOOLEAN = "java.lang.Boolean";
  private JavaFileScannerContext context;

  private static final Map ifStatementCache = new HashMap<>();
  private static final Map firstNullCheckCache = new HashMap<>();

  @Override
  public void scanFile(JavaFileScannerContext context) {
    ifStatementCache.clear();
    firstNullCheckCache.clear();
    this.context = context;
    if (context.getSemanticModel() != null) {
      scan(context.getTree());
    }
  }

  @Override
  public void visitForStatement(ForStatementTree tree) {
    if (tree.condition() != null && !isSafeBooleanExpression(tree.condition())) {
      scan(tree.initializer());
      scan(tree.update());
      scan(tree.statement());
    } else {
      super.visitForStatement(tree);
    }
  }

  @Override
  public void visitWhileStatement(WhileStatementTree tree) {
    if (!isSafeBooleanExpression(tree.condition())) {
      scan(tree.statement());
    } else {
      super.visitWhileStatement(tree);
    }
  }

  @Override
  public void visitDoWhileStatement(DoWhileStatementTree tree) {
    if (!isSafeBooleanExpression(tree.condition())) {
      scan(tree.statement());
    } else {
      super.visitDoWhileStatement(tree);
    }
  }

  @Override
  public void visitIfStatement(IfStatementTree tree) {
    if (!isSafeBooleanExpression(tree.condition())) {
      scan(tree.thenStatement());
      scan(tree.elseStatement());
    } else {
      super.visitIfStatement(tree);
    }
  }

  @Override
  public void visitConditionalExpression(ConditionalExpressionTree tree) {
    if (!isSafeBooleanExpression(tree.condition())) {
      scan(tree.trueExpression());
      scan(tree.falseExpression());
    } else {
      super.visitConditionalExpression(tree);
    }
  }

  private boolean isSafeBooleanExpression(ExpressionTree tree) {
    ExpressionTree boxedBoolean = findBoxedBoolean(tree);
    if (boxedBoolean != null) {
      // The rule is relaxed if the first usage of the variable is a test against nullness.
      // A more thorough approach would require tracing all possible paths to lookup the test using symbolic execution.
      if (isFirstUsageANullCheck(boxedBoolean)) {
        return true;
      }
      QuickFixHelper.newIssue(context)
        .forRule(this)
        .onTree(boxedBoolean)
        .withMessage(MESSAGE)
        .withQuickFixes(() -> getQuickFix(tree, boxedBoolean))
        .report();
      return false;
    }
    return true;
  }

  private static boolean isFirstUsageANullCheck(ExpressionTree boxedBoolean) {
    if (boxedBoolean.is(Kind.IDENTIFIER)) {
      IdentifierTree identifier = (IdentifierTree) boxedBoolean;
      // Usages are not guaranteed to be ordered
      List usages = identifier.symbol().usages();
      Tree firstUsage = usages.get(0).parent();
      // Test if the first usage in our list is a null check
      if (firstUsage.is(Kind.EQUAL_TO, Kind.NOT_EQUAL_TO) && isNullCheck((ExpressionTree) firstUsage)) {
        return true;
      }
      // Return false if the only usage is not a null check
      if (usages.size() == 1) {
        return false;
      }
      // Fetch the first null check in the usages list
      Optional firstNullCheck = getFirstNullCheck(identifier.symbol());
      if (!firstNullCheck.isPresent()) {
        return false;
      }
      // Test if the first null check and the first usage are part of the same higher if structure
      Optional ifStatementWithNullCheck = getParentConditionalBranch(firstNullCheck.get());
      Optional ifStatementWithFirstUsage = getParentConditionalBranch(firstUsage);
      return ifStatementWithNullCheck.equals(ifStatementWithFirstUsage);
    }
    if (boxedBoolean.is(Kind.TYPE_CAST)) {
      TypeCastTree typeCast = (TypeCastTree) boxedBoolean;
      return isFirstUsageANullCheck(typeCast.expression());
    }
    return false;
  }

  private static Optional getFirstNullCheck(Symbol symbol) {
    if (firstNullCheckCache.containsKey(symbol)) {
      return Optional.ofNullable(firstNullCheckCache.get(symbol));
    }
    Optional firstNullCheck = symbol.usages().stream()
      .map(IdentifierTree::parent)
      .filter(tree -> tree.is(Kind.EQUAL_TO, Kind.NOT_EQUAL_TO) && isNullCheck((ExpressionTree) tree))
      .map(ExpressionTree.class::cast)
      .findFirst();
    firstNullCheckCache.put(symbol, firstNullCheck.orElse(null));
    return firstNullCheck;
  }


  private static Optional getParentConditionalBranch(Tree tree) {
    Deque trees = new ArrayDeque<>();
    Tree current = tree;
    IfStatementTree ifStatementTree = null;

    while (current != null && ifStatementTree == null) {
      if (ifStatementCache.containsKey(tree)) {
        ifStatementTree = ifStatementCache.get(tree);
      } else if (current.is(Kind.IF_STATEMENT)) {
        ifStatementTree = (IfStatementTree) current;
      }
      trees.add(current);
      current = current.parent();
    }

    while (!trees.isEmpty()) {
      ifStatementCache.put(trees.pop(), ifStatementTree);
    }

    return Optional.ofNullable(ifStatementTree);
  }

  @CheckForNull
  private static ExpressionTree findBoxedBoolean(ExpressionTree tree) {
    if (tree.symbolType().is(BOOLEAN) && !isValidMethodInvocation(tree) && !isNonnullIdentifier(tree)) {
      return tree;
    }
    if (tree.is(Kind.LOGICAL_COMPLEMENT)) {
      return findBoxedBoolean(((UnaryExpressionTree) tree).expression());
    }
    if (tree instanceof BinaryExpressionTree expr) {
      if (findBoxedBoolean(expr.leftOperand()) != null && expr.rightOperand().symbolType().isPrimitive(Primitives.BOOLEAN)) {
        return expr.leftOperand();
      }
      if (findBoxedBoolean(expr.rightOperand()) != null && expr.leftOperand().symbolType().isPrimitive(Primitives.BOOLEAN) && !isNullCheck(expr.leftOperand())) {
        return expr.rightOperand();
      }
    }
    return null;
  }

  private static boolean isNullCheck(ExpressionTree tree) {
    if (tree.is(Kind.NOT_EQUAL_TO, Kind.EQUAL_TO)) {
      BinaryExpressionTree expr = (BinaryExpressionTree) tree;
      return expr.leftOperand().is(Kind.NULL_LITERAL) || expr.rightOperand().is(Kind.NULL_LITERAL);
    }
    return false;
  }

  private static boolean isValidMethodInvocation(ExpressionTree tree) {
    if (tree.is(Kind.METHOD_INVOCATION)) {
      MethodInvocationTree mit = (MethodInvocationTree) tree;
      return isOptionalInvocation(mit) || isAnnotatedNonnull(mit.methodSymbol());
    }
    return false;
  }

  private static boolean isOptionalInvocation(MethodInvocationTree mit) {
    return OPTIONAL_OR_ELSE.matches(mit) && !mit.arguments().get(0).is(Kind.NULL_LITERAL);
  }

  private static boolean isNonnullIdentifier(ExpressionTree tree) {
    return tree instanceof IdentifierTree it && isAnnotatedNonnull(it.symbol());
  }

  private static boolean isAnnotatedNonnull(Symbol symbol) {
    return symbol.metadata()
      .annotations()
      .stream()
      .map(SymbolMetadata.AnnotationInstance::symbol)
      .map(Symbol::name)
      .anyMatch(name -> "nonNull".equalsIgnoreCase(name) || "notNull".equalsIgnoreCase(name));
  }

  private static List getQuickFix(ExpressionTree tree, ExpressionTree boxedBoolean) {
    if (tree.is(Kind.METHOD_INVOCATION) && OPTIONAL_OR_ELSE.matches((MethodInvocationTree) tree)) {
      // We do not suggest a quick fix when we have an optional
      return Collections.emptyList();
    }
    List edits = new ArrayList<>(2);
    if (tree.is(Kind.LOGICAL_COMPLEMENT)) {
      edits.add(JavaTextEdit.replaceTree(((UnaryExpressionTree) tree).operatorToken(), "Boolean.FALSE.equals("));
    } else {
      edits.add(JavaTextEdit.insertBeforeTree(boxedBoolean, "Boolean.TRUE.equals("));
    }
    edits.add(JavaTextEdit.insertAfterTree(boxedBoolean, ")"));

    return Collections.singletonList(JavaQuickFix.newQuickFix(MESSAGE_QUICKFIX)
      .addTextEdits(edits)
      .build());
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy