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

org.sonar.java.checks.regex.EmptyLineRegexCheck 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.regex;

import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.MethodTreeUtils;
import org.sonar.java.model.LiteralUtils;
import org.sonarsource.analyzer.commons.regex.RegexParseResult;
import org.sonarsource.analyzer.commons.regex.ast.BoundaryTree;
import org.sonarsource.analyzer.commons.regex.ast.NonCapturingGroupTree;
import org.sonarsource.analyzer.commons.regex.ast.RegexBaseVisitor;
import org.sonarsource.analyzer.commons.regex.ast.RegexTree;
import org.sonarsource.analyzer.commons.regex.ast.SequenceTree;
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.tree.AnnotationTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.LiteralTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.ParenthesizedTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;

@Rule(key = "S5846")
public class EmptyLineRegexCheck extends AbstractRegexCheck {
  private static final String MESSAGE = "Remove MULTILINE mode or change the regex.";

  private static final String JAVA_LANG_STRING = "java.lang.String";
  private static final String JAVA_UTIL_PATTERN = "java.util.regex.Pattern";

  private static final MethodMatchers STRING_REPLACE = MethodMatchers.create()
    .ofTypes(JAVA_LANG_STRING)
    .names("replaceAll", "replaceFirst")
    .addParametersMatcher(JAVA_LANG_STRING, JAVA_LANG_STRING)
    .build();

  private static final MethodMatchers PATTERN_COMPILE = MethodMatchers.create()
    .ofTypes(JAVA_UTIL_PATTERN)
    .names("compile")
    .addParametersMatcher(JAVA_LANG_STRING)
    .addParametersMatcher(JAVA_LANG_STRING, "int")
    .build();

  private static final MethodMatchers PATTERN_MATCHER = MethodMatchers.create()
    .ofTypes(JAVA_UTIL_PATTERN)
    .names("matcher")
    .addParametersMatcher("java.lang.CharSequence")
    .build();

  private static final MethodMatchers PATTERN_FIND = MethodMatchers.create()
    .ofTypes("java.util.regex.Matcher")
    .names("find")
    .addWithoutParametersMatcher()
    .build();

  private static final MethodMatchers STRING_IS_EMPTY = MethodMatchers.create()
    .ofTypes(JAVA_LANG_STRING)
    .names("isEmpty")
    .addWithoutParametersMatcher()
    .build();

  @Override
  protected MethodMatchers getMethodInvocationMatchers() {
    // Only a few methods can contain problematic regex, we don't need to check all of them.
    return MethodMatchers.or(STRING_REPLACE, PATTERN_COMPILE);
  }

  @Override
  protected boolean filterAnnotation(AnnotationTree annotation) {
    return false;
  }

  @Override
  public void checkRegex(RegexParseResult regexForLiterals, ExpressionTree methodInvocationOrAnnotation) {
    MethodInvocationTree mit = (MethodInvocationTree) methodInvocationOrAnnotation;
    EmptyLineMultilineVisitor visitor = new EmptyLineMultilineVisitor();
    visitor.visit(regexForLiterals);
    if (visitor.containEmptyLine) {
      if (PATTERN_COMPILE.matches(mit)) {
        List stringNotTestedForEmpty = getStringNotTestedForEmpty(mit);
        if (!stringNotTestedForEmpty.isEmpty()) {
          reportWithSecondaries(mit.arguments().get(0), stringNotTestedForEmpty);
        }
      } else {
        // STRING_REPLACE case
        ExpressionTree methodSelect = mit.methodSelect();
        if (methodSelect.is(Tree.Kind.MEMBER_SELECT)
          && canBeEmpty(((MemberSelectExpressionTree) methodSelect).expression())) {
          reportIssue(mit.arguments().get(0), MESSAGE);
        }
      }
    }
  }

  private static List getStringNotTestedForEmpty(MethodInvocationTree mit) {
    Tree parent = mit.parent();
    if (parent != null && parent.is(Tree.Kind.VARIABLE)) {
      // Pattern stored in a variable, check all usage for possibly empty string
      return ((VariableTree) parent).symbol().usages().stream()
        .map(EmptyLineRegexCheck::getStringInMatcherFind)
        .filter(Optional::isPresent)
        .map(Optional::get)
        .filter(EmptyLineRegexCheck::canBeEmpty)
        .toList();
    } else {
      // Pattern can be used directly
      return getStringInMatcherFind(mit)
        .filter(EmptyLineRegexCheck::canBeEmpty)
        .map(Collections::singletonList)
        .orElseGet(Collections::emptyList);
    }
  }

  private static Optional getStringInMatcherFind(ExpressionTree mit) {
    return MethodTreeUtils.subsequentMethodInvocation(mit, PATTERN_MATCHER)
      .filter(matcherMit -> MethodTreeUtils.subsequentMethodInvocation(matcherMit, PATTERN_FIND).isPresent())
      .map(matcherMit -> matcherMit.arguments().get(0));
  }

  private static boolean canBeEmpty(Tree expressionTree) {
    if (expressionTree.is(Tree.Kind.IDENTIFIER)) {
      Symbol identifierSymbol = ((IdentifierTree) expressionTree).symbol();
      Symbol owner = identifierSymbol.owner();
      return owner != null && owner.isMethodSymbol() && identifierSymbol.usages().stream().noneMatch(EmptyLineRegexCheck::isIsEmpty);
    } else if (expressionTree.is(Tree.Kind.STRING_LITERAL, Tree.Kind.TEXT_BLOCK)) {
      return LiteralUtils.trimQuotes(((LiteralTree) expressionTree).value()).isEmpty();
    } else if (expressionTree.is(Tree.Kind.PARENTHESIZED_EXPRESSION)) {
      return canBeEmpty(((ParenthesizedTree) expressionTree).expression());
    }
    // If not sure, consider it as not empty to avoid FP.
    return false;
  }

  private static boolean isIsEmpty(IdentifierTree id) {
    return MethodTreeUtils.subsequentMethodInvocation(id, STRING_IS_EMPTY).isPresent();
  }

  private void reportWithSecondaries(Tree regex, List secondaries) {
    List secondariesLocation =
      secondaries.stream().map(secondary -> new JavaFileScannerContext.Location("This string can be empty.", secondary))
        .toList();
    reportIssue(regex, MESSAGE, secondariesLocation, null);
  }

  private static class EmptyLineMultilineVisitor extends RegexBaseVisitor {
    boolean visitedStart = false;
    boolean visitedEndAfterStart = false;
    boolean containEmptyLine = false;

    @Override
    public void visitSequence(SequenceTree tree) {
      List items = tree.getItems().stream()
        .filter(item -> !isNonCapturingWithoutChild(item))
        .toList();

      if (items.size() == 1 && items.get(0).is(RegexTree.Kind.CAPTURING_GROUP)) {
        super.visitSequence(tree);
      } else if (items.size() == 2 && items.get(0).is(RegexTree.Kind.BOUNDARY) && items.get(1).is(RegexTree.Kind.BOUNDARY)) {
        super.visitSequence(tree);
        containEmptyLine |= visitedEndAfterStart;
      }
      visitedStart = false;
    }

    @Override
    public void visitBoundary(BoundaryTree boundaryTree) {
      if (boundaryTree.activeFlags().contains(Pattern.MULTILINE)) {
        if (boundaryTree.type().equals(BoundaryTree.Type.LINE_START)) {
          visitedStart = true;
        } else if (boundaryTree.type().equals(BoundaryTree.Type.LINE_END)) {
          visitedEndAfterStart = visitedStart;
        }
      }
    }

    private static boolean isNonCapturingWithoutChild(RegexTree tree) {
      return tree.is(RegexTree.Kind.NON_CAPTURING_GROUP) && ((NonCapturingGroupTree) tree).getElement() == null;
    }

  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy