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

org.sonar.java.checks.spring.SpelExpressionCheck Maven / Gradle / Ivy

The newest version!
/*
 * SonarQube Java
 * Copyright (C) 2012-2025 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.spring;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.ObjIntConsumer;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.ExpressionsHelper;
import org.sonar.java.model.DefaultJavaFileScannerContext;
import org.sonar.java.reporting.AnalyzerMessage;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.location.Position;
import org.sonar.plugins.java.api.tree.AnnotationTree;
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;

@Rule(key = "S6857")
public class SpelExpressionCheck extends IssuableSubscriptionVisitor {

  private static final String SPRING_PREFIX = "org.springframework";

  /**
   * Regular expression for a property placeholder segment that is not a SpEL expression.
   * It implements the following grammar with possessive quantifiers:
   * 

*

   * PropertyPlaceholder ::= Identifier IndexExpression* ("." Identifier IndexExpression*)*
   * Identifier          ::= [a-zA-Z0-9_-]+
   * IndexExpression     ::= "[" [0-9]+ "]"
   * 
*

* Some examples for accepted inputs: *

*

   * foo
   * foo.bar
   * foo[42].bar23
   * bar[23][42]
   * 
*/ private static final Pattern PROPERTY_PLACEHOLDER_PATTERN = Pattern.compile( "[a-zA-Z0-9/_-]++(\\[\\d++])*+(\\.[a-zA-Z0-9/_-]++(\\[\\d++])*+)*+" ); public List nodesToVisit() { return List.of(Tree.Kind.CLASS, Tree.Kind.INTERFACE); } @Override public void visitNode(Tree tree) { getClassAndMemberAnnotations((ClassTree) tree) .filter(SpelExpressionCheck::isSpringAnnotation) .forEach(this::checkSpringAnnotationArguments); } private static Stream getClassAndMemberAnnotations(ClassTree cls) { return Stream.concat( Stream.of(cls.modifiers().annotations()), cls.members().stream().map(SpelExpressionCheck::getMemberAnnotations) ).flatMap(Collection::stream); } private static List getMemberAnnotations(Tree member) { if (member.is(Tree.Kind.METHOD)) { return ((MethodTree) member).modifiers().annotations(); } else if (member.is(Tree.Kind.VARIABLE)) { return ((VariableTree) member).modifiers().annotations(); } else { return Collections.emptyList(); } } private static boolean isSpringAnnotation(AnnotationTree annotation) { return annotation.symbolType().fullyQualifiedName().startsWith(SPRING_PREFIX); } private void checkSpringAnnotationArguments(AnnotationTree annotation) { annotation.arguments().stream().map(SpelExpressionCheck::extractArgumentValue).filter(Objects::nonNull) .forEach(this::checkSpringExpressionsInString); } @CheckForNull private static Map.Entry extractArgumentValue(ExpressionTree expression) { expression = getExpressionOrAssignmentRhs(expression); var stringValue = ExpressionsHelper.getConstantValueAsString(expression).value(); if (stringValue == null) { return null; } return Map.entry(expression, stringValue); } private static ExpressionTree getExpressionOrAssignmentRhs(ExpressionTree expression) { return expression.is(Tree.Kind.ASSIGNMENT) ? ((AssignmentExpressionTree) expression).expression() : expression; } private void checkSpringExpressionsInString(Map.Entry entry) { var expression = entry.getKey(); try { var argValue = entry.getValue(); if (expression.is(Tree.Kind.STRING_LITERAL)) { checkStringContents(argValue, 1); } else { checkStringContents(argValue, 0); } } catch (SyntaxError e) { reportIssue(expression, e); } } private void reportIssue(Tree expression, SyntaxError error) { if (expression.is(Tree.Kind.STRING_LITERAL)) { // For string literals, report exact issue location within the string. var tokenStart = Position.startOf(expression); var textSpan = new AnalyzerMessage.TextSpan( tokenStart.line(), tokenStart.columnOffset() + error.startColumn, tokenStart.line(), tokenStart.columnOffset() + error.endColumn ); var analyzerMessage = new AnalyzerMessage(this, context.getInputFile(), textSpan, error.getMessage(), 0); ((DefaultJavaFileScannerContext) context).reportIssue(analyzerMessage); } else { reportIssue(expression, error.getMessage()); } } private static void checkStringContents(String content, int startColumn) throws SyntaxError { var i = 0; while (i < content.length()) { var c = content.charAt(i); switch (c) { case '$': i = parseDelimitersAndContents(content, i + 1, startColumn + i, SpelExpressionCheck::parseValidPropertyPlaceholder); break; case '#': i = parseDelimitersAndContents(content, i + 1, startColumn + i, SpelExpressionCheck::parseValidSpelExpression); break; default: i++; break; } } } /** * Parses the following grammatical expression, starting at startIndex in `value`: * *
   * ('{' contents '}')?
   * 
*

* Where correct bracing is checked and then contents is parsed using the given parseContents function. * * @param value string containing the character sequence to parse * @param startIndex index of the opening delimiter we start from in value * @param startColumn offset with the position of value within a potentially longer original string (used for reporting) * @param parseContents function to parse contents * @throws SyntaxError when the input does not comply with the expected grammatical expression */ private static int parseDelimitersAndContents( String value, int startIndex, int startColumn, ObjIntConsumer parseContents ) throws SyntaxError { if (startIndex == value.length()) { return startIndex; } var endIndex = parseDelimiterBraces(value, startIndex, startColumn); if (endIndex == startIndex) { return endIndex; } var contents = value.substring(startIndex + 1, endIndex - 1); parseContents.accept(contents, startColumn); return endIndex; } private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError { if (value.charAt(startIndex) != '{') { return startIndex; } int openCount = 1; for (var i = startIndex + 1; i < value.length(); i++) { var c = value.charAt(i); if (c == '{') { openCount++; } else if (c == '}') { openCount--; if (openCount == 0) { return i + 1; } } } // +1 because of prefix `$` or `#` var endColumn = startColumn + value.length() - startIndex + 1; throw new SyntaxError("Add missing '}' for this property placeholder or SpEL expression.", startColumn, endColumn); } private static void parseValidPropertyPlaceholder(String placeholder, int startColumn) throws SyntaxError { if (!isValidPropertyPlaceholder(placeholder, startColumn)) { // +3 because of delimiter `#{` and `}` var endColumn = startColumn + placeholder.length() + 3; throw new SyntaxError("Correct this malformed property placeholder.", startColumn, endColumn); } } private static boolean isValidPropertyPlaceholder(String placeholder, int startColumn) throws SyntaxError { var segments = placeholder.split(":",2); if (!isValidPropertyPlaceholderFirstSegment(segments[0], startColumn)) { return false; } return segments.length < 2 || (isValidPropertyPlaceholderDefaultSegment(segments[1], startColumn + segments[0].length() + 1)); } private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError { var stripped = segment.stripLeading(); startColumn += segment.length() - stripped.length(); stripped = stripped.stripTrailing(); if (stripped.startsWith("#{")) { parseDelimitersAndContents(stripped, 1, startColumn + 2, SpelExpressionCheck::parseValidSpelExpression); return true; } else { return PROPERTY_PLACEHOLDER_PATTERN.matcher(stripped).matches(); } } private static boolean isValidPropertyPlaceholderDefaultSegment(String segment, int startColumn) throws SyntaxError { var stripped = segment.stripLeading(); startColumn += segment.length() - stripped.length(); stripped = stripped.stripTrailing(); var contentsParser = getContentsParser(stripped); if (contentsParser != null) { var endIndex = parseDelimitersAndContents(stripped, 1, startColumn + 2, contentsParser); return endIndex == segment.stripTrailing().length(); } return true; } private static ObjIntConsumer getContentsParser(String contents) { if (contents.startsWith("${")) { return SpelExpressionCheck::parseValidPropertyPlaceholder; } if (contents.startsWith("#{")) { return SpelExpressionCheck::parseValidSpelExpression; } return null; } private static void parseValidSpelExpression(String expressionString, int startColumn) throws SyntaxError { if (!isValidSpelExpression(expressionString)) { // +3 because of delimiter `${` and `}` var endColumn = startColumn + expressionString.length() + 3; throw new SyntaxError("Correct this malformed SpEL expression.", startColumn, endColumn); } } private static boolean isValidSpelExpression(String expressionString) { expressionString = expressionString.strip(); if (expressionString.isEmpty()) { return false; } try { new SpelExpressionParser().parseExpression(expressionString); } catch (ParseException | IllegalStateException e) { return false; } return true; } private static class SyntaxError extends RuntimeException { SyntaxError(String message, int startColumn, int endColumn) { super(message); this.startColumn = startColumn; this.endColumn = endColumn; } public final int startColumn; public final int endColumn; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy