
org.sonar.java.checks.regex.RegexComplexityCheck Maven / Gradle / Ivy
/*
* 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.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.java.model.LineUtils;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.tree.BinaryExpressionTree;
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.SyntaxTrivia;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonarsource.analyzer.commons.regex.RegexParseResult;
import org.sonarsource.analyzer.commons.regex.ast.FlagSet;
import org.sonarsource.analyzer.commons.regex.finders.ComplexRegexFinder;
@Rule(key = "S5843")
public class RegexComplexityCheck extends AbstractRegexCheck {
private static final int DEFAULT_MAX = 20;
@RuleProperty(
key = "maxComplexity",
description = "The maximum authorized complexity.",
defaultValue = "" + DEFAULT_MAX)
private int max = DEFAULT_MAX;
private final List regexConstructions = new ArrayList<>();
private final Set commentedLines = new HashSet<>();
@Override
public List nodesToVisit() {
List nodes = new ArrayList<>(super.nodesToVisit());
nodes.add(Tree.Kind.TRIVIA);
return nodes;
}
@Override
public void checkRegex(RegexParseResult parseResult, ExpressionTree methodInvocationOrAnnotation) {
// The parse result is not used except to get the initial flags. We find and parse the parts of the regex
// ourselves because we want to count the complexity of each part individually if the regex is made out of
// parts stored in variables.
ExpressionTree regexArgument = getRegexLiteralExpression(methodInvocationOrAnnotation);
// regexArgument can not be null when "checkRegex" is called
regexConstructions.add(new RegexConstructionInfo(regexArgument, parseResult.getInitialFlags(), parseResult.containsComments()));
}
@Override
public void visitTrivia(SyntaxTrivia syntaxTrivia) {
int startLine = LineUtils.startLine(syntaxTrivia);
commentedLines.add(startLine);
int numLines = StringUtils.countMatches(syntaxTrivia.comment(), "\n");
if (numLines > 0) {
commentedLines.add(startLine + numLines);
}
}
@Override
public void leaveFile(JavaFileScannerContext context) {
for (RegexConstructionInfo regexInfo : regexConstructions) {
FlagSet flags = regexInfo.initialFlags;
for (LiteralTree[] regexPart : findRegexParts(regexInfo)) {
new ComplexRegexFinder(this::reportIssueFromCommons, max).visit(regexForLiterals(flags, regexPart));
}
}
regexConstructions.clear();
commentedLines.clear();
}
List findRegexParts(RegexConstructionInfo regexInfo) {
RegexPartFinder finder = new RegexPartFinder(regexInfo.initialFlags, regexInfo.containsComments);
finder.find(regexInfo.regexArgument);
return finder.parts;
}
public void setMax(int max) {
this.max = max;
}
private class RegexPartFinder {
final FlagSet initialFlags;
final boolean regexContainsComments;
List parts = new ArrayList<>();
RegexPartFinder(FlagSet initialFlags, boolean regexContainsComments) {
this.initialFlags = initialFlags;
this.regexContainsComments = regexContainsComments;
}
void find(ExpressionTree expr) {
switch (expr.kind()) {
case PLUS:
List literals = new ArrayList<>();
findInStringConcatenation(expr, literals);
if (!literals.isEmpty()) {
parts.add(literals.toArray(new LiteralTree[0]));
}
break;
case IDENTIFIER:
getFinalVariableInitializer((IdentifierTree) expr).ifPresent(this::find);
break;
case PARENTHESIZED_EXPRESSION:
find(ExpressionUtils.skipParentheses(expr));
break;
case STRING_LITERAL:
parts.add(new LiteralTree[] {(LiteralTree) expr});
break;
default:
// Do nothing
}
}
void findInStringConcatenation(ExpressionTree expr, List literals) {
if (expr.is(Tree.Kind.STRING_LITERAL)) {
LiteralTree literal = (LiteralTree) expr;
if (isCommented(literal)) {
parts.add(new LiteralTree[] {literal});
} else {
literals.add(literal);
}
} else if (expr.is(Tree.Kind.PLUS)) {
BinaryExpressionTree binExpr = (BinaryExpressionTree) expr;
findInStringConcatenation(binExpr.leftOperand(), literals);
findInStringConcatenation(binExpr.rightOperand(), literals);
} else if (expr.is(Tree.Kind.PARENTHESIZED_EXPRESSION)) {
findInStringConcatenation(ExpressionUtils.skipParentheses(expr), literals);
} else {
find(expr);
}
}
private boolean isCommented(LiteralTree regexPart) {
int line = LineUtils.startLine(regexPart);
return regexContainsComments
|| commentedLines.contains(line)
|| commentedLines.contains(line - 1);
}
}
private static class RegexConstructionInfo {
final ExpressionTree regexArgument;
final FlagSet initialFlags;
final boolean containsComments;
RegexConstructionInfo(ExpressionTree regexArgument, FlagSet initialFlags, boolean containsComments) {
this.regexArgument = regexArgument;
this.initialFlags = initialFlags;
this.containsComments = containsComments;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy