Repository: SonarSource/sonar-java
Pull request #5128: SONARJAVA-4917 S6857 support property placeholder inside SpEL
Author: login: erwan-serandour
Created: 2025-05-05T14:05:52Z | Merged: 2025-05-06T13:05:25Z

[SONARJAVA-4917](https://sonarsource.atlassian.net/browse/SONARJAVA-4917)



[SONARJAVA-4917]: https://sonarsource.atlassian.net/browse/SONARJAVA-4917?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S6857.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S6857.json
index becb4d249d3..d88b66b52da 100644
--- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S6857.json
+++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S6857.json
@@ -1,6 +1,6 @@
 {
   "ruleKey": "S6857",
   "hasTruePositives": false,
-  "falseNegatives": 63,
+  "falseNegatives": 64,
   "falsePositives": 0
 }
diff --git a/java-checks-test-sources/default/src/main/java/checks/spring/SpelExpressionCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/spring/SpelExpressionCheckSample.java
index 89feae57da8..33d55ed3825 100644
--- a/java-checks-test-sources/default/src/main/java/checks/spring/SpelExpressionCheckSample.java
+++ b/java-checks-test-sources/default/src/main/java/checks/spring/SpelExpressionCheckSample.java
@@ -1,6 +1,7 @@
 package checks.spring;
 
 import java.util.List;
+import java.util.Map;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.mongodb.repository.Query;
 import org.springframework.data.repository.query.Param;
@@ -14,6 +15,7 @@ public class SpelExpressionCheckSample {
   private static final String INVALID_PROPERTY_PLACEHOLDER = "${foo.bar[}";
   private static final String VALID_PROPERTY_PLACEHOLDER = "${foo.bar}";
 
+
   @Value(UNCLOSED) // Noncompliant {{Add missing '}' for this property placeholder or SpEL expression.}}
 //       ^^^^^^^^
   private String complexArgument1;
@@ -100,15 +102,15 @@ public class SpelExpressionCheckSample {
   @Value("${user.region:#{'D'+'E'}}") // Compliant
   private String default10;
 
-  @Value("${user.region:#{null}:#{null}:foo.bar}") // Noncompliant
+  @Value("${user.region:#{null}:#{null}:foo.bar}") // Compliant
   private String default11;
 
-  @Value("${user.region:#{null}:#{4**4}:foo.bar}") // Noncompliant {{Correct this malformed property placeholder.}}
-//        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  @Value("${user.region:#{null}:#{4**4}:foo.bar}") // Noncompliant {{Correct this malformed SpEL expression.}}
+  //                            ^^^^^^^
   private String default12;
 
   @Value("${user.region:#{4**4}:#{null}:foo.bar}") // Noncompliant {{Correct this malformed SpEL expression.}}
-//                      ^^^^^^^
+
   private String default13;
 
   @Value("${user.2region:default-region}") // Compliant
@@ -441,4 +443,49 @@ public static class RequestController2 { }
   private String sonarJava5079DefaultValueContainsColon2;
 
   private static final String MOCKED_SOAP_SP_CLIENT_SSL = "classpath:mocked-soap-sp-client-ssl.jks";
+
+  static class PropertyPlaceHolderInsideSpEL {
+    // We want to parse correctly the expression below
+    @Value("#{${placeholder}}") // Compliant
+    private Map<String, Integer> placeholderStage1;
+
+    // However, the problem is that Spring evaluates the expression in two stages
+    // 1. it parses the property placeholders and substitutes them
+    // 2. it parses the SpEL expression and executes it
+
+    // After evaluating the property placeholder, we have the following expression
+    @Value("#{{key1: '1', key2: '2', key3: '3'}}") // Compliant
+    private Map<String, Integer> SpELStage1;
+
+    // It is easy to validate that property placeholders are valid, but without the context we cannot validate the spring expression
+    // A solution is to use clever values to replace the property placeholders for instance replace "${placeholder}" with  "#aVar"
+    @Value("#{${placeholder}}") // Compliant
+    private Integer placeholderStage2;
+    @Value("#{#aVar}") // Compliant
+    private Integer SpELStage2;
+
+    // However, it is not possible to find a value that is valid in all the cases.
+    // "#aVar" does not work in the case below.
+    @Value("#{@${placeholder}}") // Compliant
+    private Integer placeholderStage3;
+    // "@#aVar" is not a valid SpEL expression.
+    @Value("#{@#aVar}") // Noncompliant
+    private Integer SpELStage3;
+
+    @Value("#{@${placeholder}}") // Compliant
+    private Integer placeholderStage4;
+    @Value("#{@aBean}") // Compliant
+    private Integer SpELStage4;
+
+    @Value("#{${placeholder:10}}") // Compliant
+    private Integer placeholderStage5;
+    @Value("#{10}") // Compliant
+    private Integer SpELStage5;
+
+
+    @Value("#{@${placeholder:10}}") // Compliant
+    private Integer placeholderStage6;
+    @Value("#{@10}") // Noncompliant
+    private Integer SpELStage6;
+  }
 }
diff --git a/java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java b/java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
index d94dcddef29..404fe6361c9 100644
--- a/java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
+++ b/java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
@@ -16,15 +16,16 @@
  */
 package org.sonar.java.checks.spring;
 
+import java.io.Serializable;
 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 javax.annotation.Nullable;
 import org.sonar.check.Rule;
 import org.sonar.java.checks.helpers.ExpressionsHelper;
 import org.sonar.java.model.DefaultJavaFileScannerContext;
@@ -66,8 +67,7 @@ public class SpelExpressionCheck extends IssuableSubscriptionVisitor {
    * </pre>
    */
   private static final Pattern PROPERTY_PLACEHOLDER_PATTERN = Pattern.compile(
-    "[a-zA-Z0-9/_-]++(\\[\\d++])*+(\\.[a-zA-Z0-9/_-]++(\\[\\d++])*+)*+"
-  );
+    "[a-zA-Z0-9/_-]++(\\[\\d++])*+(\\.[a-zA-Z0-9/_-]++(\\[\\d++])*+)*+");
 
   public List<Tree.Kind> nodesToVisit() {
     return List.of(Tree.Kind.CLASS, Tree.Kind.INTERFACE);
@@ -83,8 +83,7 @@ public void visitNode(Tree tree) {
   private static Stream<AnnotationTree> getClassAndMemberAnnotations(ClassTree cls) {
     return Stream.concat(
       Stream.of(cls.modifiers().annotations()),
-      cls.members().stream().map(SpelExpressionCheck::getMemberAnnotations)
-    ).flatMap(Collection::stream);
+      cls.members().stream().map(SpelExpressionCheck::getMemberAnnotations)).flatMap(Collection::stream);
   }
 
   private static List<AnnotationTree> getMemberAnnotations(Tree member) {
@@ -125,9 +124,9 @@ private void checkSpringExpressionsInString(Map.Entry<Tree, String> entry) {
     try {
       var argValue = entry.getValue();
       if (expression.is(Tree.Kind.STRING_LITERAL)) {
-        checkStringContents(argValue, 1);
+        checkStringContents(new ParseCtx(argValue, 1));
       } else {
-        checkStringContents(argValue, 0);
+        checkStringContents(new ParseCtx(argValue, 0));
       }
     } catch (SyntaxError e) {
       reportIssue(expression, e);
@@ -140,10 +139,9 @@ private void reportIssue(Tree expression, SyntaxError error) {
       var tokenStart = Position.startOf(expression);
       var textSpan = new AnalyzerMessage.TextSpan(
         tokenStart.line(),
-        tokenStart.columnOffset() + error.startColumn,
+        tokenStart.columnOffset() + error.range.start,
         tokenStart.line(),
-        tokenStart.columnOffset() + error.endColumn
-      );
+        tokenStart.columnOffset() + error.range.end);
 
       var analyzerMessage = new AnalyzerMessage(this, context.getInputFile(), textSpan, error.getMessage(), 0);
       ((DefaultJavaFileScannerContext) context).reportIssue(analyzerMessage);
@@ -152,162 +150,224 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(ParseCtx ctx) throws SyntaxError {
+    String expressionSource = ctx.expressionSource();
+    int idx = 0;
+
+    while (idx < expressionSource.length()) {
+      if (PropertyPlaceholder.matchPrefix(expressionSource, idx)) {
+        var placeholder = PropertyPlaceholder.parse(ctx, idx);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(expressionSource, idx)) {
+        var expr = SpEL.parse(ctx, idx);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
+  private static class SyntaxError extends RuntimeException {
+
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
+    }
+
+    public final Range range;
+  }
+
   /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
+   * Represents a non-empty range
    *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
+   * @param start the inclusive start index of the range
+   * @param end   the exclusive end index of the range
    */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> parseContents
-  ) throws SyntaxError {
-    if (startIndex == value.length()) {
-      return startIndex;
+  private record Range(int start, int end) implements Serializable {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException("Range must be non-empty; start value must be less than end value.");
+      }
     }
-    var endIndex = parseDelimiterBraces(value, startIndex, startColumn);
-    if (endIndex == startIndex) {
-      return endIndex;
+
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
-    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;
+  /**
+   * Represents a context for parsing expressions.
+   *
+   * @param expressionSource the expression source text
+   * @param offset           offset represents the start of the expression text with respect to the literal string containing it.
+   */
+  private record ParseCtx(String expressionSource, int offset) {
+  }
+
+  private static class PropertyPlaceholder {
+    private static final char PREFIX = '$';
+
+    static boolean matchPrefix(String expressionSource, int idx) {
+      return idx + 1 < expressionSource.length() && expressionSource.charAt(idx) == PREFIX && expressionSource.charAt(idx + 1) == '{';
     }
 
-    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;
+    /**
+     * Parses a property placeholder from the given string starting at the specified index.
+     * Assumes that the prefix for a property placeholder is already matched.
+     *
+     * For example, given the inputs:
+     * - ctx=ParseCtx("1234${foo.property:default}", offset=0)
+     * - startIdx=4
+     * parse will return: Placeholder(offset=0, range=Range(4, 27), expr="foo.property", defaultValue="default")
+     *
+     * @param ctx The context of the parsing.
+     * @param startIdx The starting index of the placeholder in the string.
+     * @return A {@link Placeholder} object representing the parsed placeholder.
+     * @throws IllegalArgumentException If the prefix is not matched.
+     * @throws SyntaxError If the placeholder is malformed or if matching braces are not closed.
+     *
+     */
+    static Placeholder parse(ParseCtx ctx, int startIdx) {
+      String expressionSource = ctx.expressionSource();
+      if (!PropertyPlaceholder.matchPrefix(expressionSource, startIdx)) {
+        throw new IllegalArgumentException();
+      }
+
+      ParseStates state = new Expr();
+      int startExpr = startIdx + 2;
+
+      for (int idx = startExpr; idx < expressionSource.length(); idx++) {
+        char current = expressionSource.charAt(idx);
+
+        if (state instanceof Expr && (current == '}' || current == ':')) {
+          String expr = expressionSource.substring(startExpr, idx).trim();
+          if (current == '}') {
+            return new Placeholder(ctx.offset(), new Range(startIdx, idx + 1), expr, null);
+          } else {
+            state = new DefaultValue(0, expr, idx + 1);
+          }
+        } else if (state instanceof DefaultValue d && current == '}' && d.nestingLevel == 0) {
+          return new Placeholder(ctx.offset(), new Range(startIdx, idx + 1), d.expr, expressionSource.substring(d.startDefault, idx).trim());
+        } else if (state instanceof DefaultValue d) {
+          if (SpEL.matchPrefix(expressionSource, idx)) {
+            SpEL.parse(ctx, idx);
+          } else if (current == '{') {
+            state = d.increaseNestingLevel();
+          } else if (current == '}') {
+            state = d.decreaseNestingLevel();
+          }
         }
       }
-    }
 
-    // +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);
-  }
+      Range range = new Range(startIdx, expressionSource.length());
+      throw new SyntaxError("Add missing '}' for this property placeholder or SpEL expression.", range.addOffset(ctx.offset()));
+    }
 
-  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);
+    sealed interface ParseStates {
     }
-  }
+    record Expr() implements ParseStates {
+    }
+    record DefaultValue(int nestingLevel, String expr, int startDefault) implements ParseStates {
+      DefaultValue increaseNestingLevel() {
+        return new DefaultValue(nestingLevel + 1, expr, startDefault);
+      }
 
-  private static boolean isValidPropertyPlaceholder(String placeholder, int startColumn) throws SyntaxError {
-    var segments = placeholder.split(":",2);
-    if (!isValidPropertyPlaceholderFirstSegment(segments[0], startColumn)) {
-      return false;
+      DefaultValue decreaseNestingLevel() {
+        return new DefaultValue(nestingLevel - 1, expr, startDefault);
+      }
     }
-    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();
+  record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
+    public Placeholder {
+      if (!PROPERTY_PLACEHOLDER_PATTERN.asMatchPredicate().test(expr)) {
+        throw new SyntaxError("Correct this malformed property placeholder.", range.addOffset(offset));
+      }
+    }
 
-    if (stripped.startsWith("#{")) {
-      parseDelimitersAndContents(stripped, 1, startColumn + 2, SpelExpressionCheck::parseValidSpelExpression);
-      return true;
-    } else {
-      return PROPERTY_PLACEHOLDER_PATTERN.matcher(stripped).matches();
+    /**
+     * Evaluates the placeholder and returns its evaluation using a heuristic.
+     * We cannot return the true value without access to the context.
+     *
+     * @param expressionSource The original string containing the placeholder.
+     * @return The evaluated string value of the placeholder.
+     */
+    public String evaluate(String expressionSource) {
+      char before = range.start > 0 ? expressionSource.charAt(range.start - 1) : ' ';
+      if (before == '@') {
+        return "bean";
+      }
+      return "#var";
     }
   }
 
-  private static boolean isValidPropertyPlaceholderDefaultSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+  private static class SpEL {
+    private static final char PREFIX = '#';
 
-    var contentsParser = getContentsParser(stripped);
-    if (contentsParser != null) {
-      var endIndex = parseDelimitersAndContents(stripped, 1, startColumn + 2, contentsParser);
-      return endIndex == segment.stripTrailing().length();
+    static boolean matchPrefix(String expressionSource, int startIdx) {
+      return startIdx + 1 < expressionSource.length() && expressionSource.charAt(startIdx) == PREFIX && expressionSource.charAt(startIdx + 1) == '{';
     }
-    return true;
-  }
 
-  private static ObjIntConsumer<String> getContentsParser(String contents) {
-    if (contents.startsWith("${")) {
-      return SpelExpressionCheck::parseValidPropertyPlaceholder;
-    }
-    if (contents.startsWith("#{")) {
-      return SpelExpressionCheck::parseValidSpelExpression;
-    }
-    return null;
-  }
+    /**
+     * Parses a SpEL (Spring Expression Language) expression from the given string starting at the specified index.
+     * Assumes that the prefix for a SpEL expression is already matched.
+     *
+     * For example, given the inputs:
+     * - ctx=ParseCtx(expressionSource="1234#{1 + 2}", offset=0)
+     * - startIdx=4
+     * parse will return: SpELExpr(offset=0, range=Range(4, 12), expr="1 + 2")
+     *
+     * @param ctx The context of the parsing.
+     * @param startIdx The starting index of the SpEL expression in the string.
+     * @return A {@link SpELExpr} object representing the parsed SpEL expression.
+     * @throws IllegalArgumentException If the prefix is not matched.
+     * @throws SyntaxError If the SpEL expression is malformed or if matching braces are not closed.
+     */
+    static SpELExpr parse(ParseCtx ctx, int startIdx) {
+      String expressionSource = ctx.expressionSource();
+      if (!SpEL.matchPrefix(expressionSource, startIdx)) {
+        throw new IllegalArgumentException();
+      }
 
-  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);
-    }
-  }
+      int startExpr = startIdx + 2;
+      StringBuilder evaluated = new StringBuilder();
+      int nestingLevel = 0;
+      int idx = startExpr;
+
+      while (idx < expressionSource.length()) {
+        char current = expressionSource.charAt(idx);
+
+        if (SpelExpressionCheck.PropertyPlaceholder.matchPrefix(expressionSource, idx)) {
+          var placeholder = SpelExpressionCheck.PropertyPlaceholder.parse(ctx, idx);
+          evaluated.append(placeholder.evaluate(expressionSource));
+          idx = placeholder.range.end;
+        } else if (current == '}' && nestingLevel == 0) {
+
+          return new SpELExpr(evaluated.toString(), new Range(startIdx, idx + 1), ctx.offset());
+        } else {
+          evaluated.append(expressionSource.charAt(idx));
+          if (current == '{') {
+            nestingLevel++;
+          } else if (current == '}') {
+            nestingLevel--;
+          }
+          idx++;
+        }
+      }
 
-  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;
+      Range range = new Range(startIdx, expressionSource.length());
+      throw new SyntaxError("Add missing '}' for this property placeholder or SpEL expression.", range.addOffset(ctx.offset()));
     }
-    return true;
-  }
 
-  private static class SyntaxError extends RuntimeException {
+  }
 
-    SyntaxError(String message, int startColumn, int endColumn) {
-      super(message);
-      this.startColumn = startColumn;
-      this.endColumn = endColumn;
+  record SpELExpr(String expr, Range range, int offset) {
+    public SpELExpr {
+      try {
+        new SpelExpressionParser().parseExpression(expr);
+      } catch (ParseException | IllegalStateException | IllegalArgumentException e) {
+        throw new SyntaxError("Correct this malformed SpEL expression.", range.addOffset(offset));
+      }
     }
-
-    public final int startColumn;
-    public final int endColumn;
   }
 }


Reviews:
user: login: romainbrenguier
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:28:00Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:42:32Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:43:03Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:43:50Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:50:12Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:52:33Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:52:44Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T09:01:01Z
user: login: erwan-serandour
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T09:10:53Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request implements support for property placeholders inside SpEL expressions to address SONARJAVA-4917. Key changes include refactoring the string content checking methods to use a new ParseCtx, introducing new record types (Range, Placeholder, and ParseCtx), and updating error messaging and parsing logic for property placeholders and SpEL expressions.

### Reviewed Changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated no comments.

| File | Description |
| ---- | ----------- |
| java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java | Refactored parsing logic and error reporting for property placeholders and SpEL expressions using new types and methods. |
| java-checks-test-sources/default/src/main/java/checks/spring/SpelExpressionCheckSample.java | Updated test cases to reflect the new parsing behavior and error messages for property placeholders inside SpEL. |

<details>
<summary>Files not reviewed (1)</summary>

* **its/autoscan/src/test/resources/autoscan/diffs/diff_S6857.json**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java:188**
* [nitpick] Consider rephrasing the exception message to improve clarity and grammatical correctness (e.g., 'Range must be non-empty; start value must be less than end value.').
```
if (start >= end) { throw new IllegalArgumentException("a range is must be non empty, this imply start must be less than end"); }
```
**java-checks-test-sources/default/src/main/java/checks/spring/SpelExpressionCheckSample.java:447**
* [nitpick] Consider renaming 'PropertyPlaceHolderInsideSpEL' to 'PropertyPlaceholderInsideSpEL' for consistency in capitalization.
```
static class PropertyPlaceHolderInsideSpEL {
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-06T10:03:55Z
user: login: romainbrenguier
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T11:53:31Z

Review comments:
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
 
-  private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
+      public Placeholder {
+        if (!PROPERTY_PLACEHOLDER_PATTERN.asMatchPredicate().test(expr)) {
+          throw new SyntaxError("Correct this malformed property placeholder.", range.addOffset(offset));
+        }
+      }
 
-    if (stripped.startsWith("#{")) {
-      parseDelimitersAndContents(stripped, 1, startColumn + 2, SpelExpressionCheck::parseValidSpelExpression);
-      return true;
-    } else {
-      return PROPERTY_PLACEHOLDER_PATTERN.matcher(stripped).matches();
+      public String evaluate(String stream) {
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: romainbrenguier
body: Some javadoc would be usefull
created_at: 2025-05-06T07:14:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
 
-  private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: romainbrenguier
body: That's a bit too much nesting for me. Could we make the class structure a bit more flat.
created_at: 2025-05-06T07:14:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: romainbrenguier
body: final
created_at: 2025-05-06T07:14:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: romainbrenguier
body: You can maybe add a message in the exception to explain the assumption.
created_at: 2025-05-06T07:14:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
 
-  private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
+      public Placeholder {
+        if (!PROPERTY_PLACEHOLDER_PATTERN.asMatchPredicate().test(expr)) {
+          throw new SyntaxError("Correct this malformed property placeholder.", range.addOffset(offset));
+        }
+      }
 
-    if (stripped.startsWith("#{")) {
-      parseDelimitersAndContents(stripped, 1, startColumn + 2, SpelExpressionCheck::parseValidSpelExpression);
-      return true;
-    } else {
-      return PROPERTY_PLACEHOLDER_PATTERN.matcher(stripped).matches();
+      public String evaluate(String stream) {
+        char before = range.start > 0 ? stream.charAt(range.start - 1) : ' ';
+        if (before == '@') {
+          return "bean";
+        }
+        return "#var";
+      }
     }
-  }
 
-  private static boolean isValidPropertyPlaceholderDefaultSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    static boolean matchPrefix(String stream, int idx) {
+      return idx + 1 < stream.length() && stream.charAt(idx) == prefix && stream.charAt(idx + 1) == '{';
+    }
+
+    /**
+     * Parses a property placeholder from the given string starting at the specified index.
+     * Assumes that the prefix for a property placeholder is already matched.
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: romainbrenguier
body: An example would be nice
created_at: 2025-05-06T07:27:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
 
-  private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
+      public Placeholder {
+        if (!PROPERTY_PLACEHOLDER_PATTERN.asMatchPredicate().test(expr)) {
+          throw new SyntaxError("Correct this malformed property placeholder.", range.addOffset(offset));
+        }
+      }
 
-    if (stripped.startsWith("#{")) {
-      parseDelimitersAndContents(stripped, 1, startColumn + 2, SpelExpressionCheck::parseValidSpelExpression);
-      return true;
-    } else {
-      return PROPERTY_PLACEHOLDER_PATTERN.matcher(stripped).matches();
+      public String evaluate(String stream) {
+        char before = range.start > 0 ? stream.charAt(range.start - 1) : ' ';
+        if (before == '@') {
+          return "bean";
+        }
+        return "#var";
+      }
     }
-  }
 
-  private static boolean isValidPropertyPlaceholderDefaultSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    static boolean matchPrefix(String stream, int idx) {
+      return idx + 1 < stream.length() && stream.charAt(idx) == prefix && stream.charAt(idx + 1) == '{';
+    }
+
+    /**
+     * Parses a property placeholder from the given string starting at the specified index.
+     * Assumes that the prefix for a property placeholder is already matched.
+     *
+     * @param stream The input string containing the property placeholder.
+     * @param startIdx The starting index of the placeholder in the string.
+     * @param offset The offset to be added to the range for error reporting.
+     * @return A {@link Placeholder} object representing the parsed placeholder.
+     * @throws IllegalArgumentException If the prefix is not matched.
+     * @throws SyntaxError If the placeholder is malformed or if matching braces are not closed.
+     */
+    static Placeholder parse(String stream, int startIdx, int offset) {
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: romainbrenguier
body: naming the string "stream" is a bit confusing
created_at: 2025-05-06T07:27:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
 
-  private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
+      public Placeholder {
+        if (!PROPERTY_PLACEHOLDER_PATTERN.asMatchPredicate().test(expr)) {
+          throw new SyntaxError("Correct this malformed property placeholder.", range.addOffset(offset));
+        }
+      }
 
-    if (stripped.startsWith("#{")) {
-      parseDelimitersAndContents(stripped, 1, startColumn + 2, SpelExpressionCheck::parseValidSpelExpression);
-      return true;
-    } else {
-      return PROPERTY_PLACEHOLDER_PATTERN.matcher(stripped).matches();
+      public String evaluate(String stream) {
+        char before = range.start > 0 ? stream.charAt(range.start - 1) : ' ';
+        if (before == '@') {
+          return "bean";
+        }
+        return "#var";
+      }
     }
-  }
 
-  private static boolean isValidPropertyPlaceholderDefaultSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    static boolean matchPrefix(String stream, int idx) {
+      return idx + 1 < stream.length() && stream.charAt(idx) == prefix && stream.charAt(idx + 1) == '{';
+    }
+
+    /**
+     * Parses a property placeholder from the given string starting at the specified index.
+     * Assumes that the prefix for a property placeholder is already matched.
+     *
+     * @param stream The input string containing the property placeholder.
+     * @param startIdx The starting index of the placeholder in the string.
+     * @param offset The offset to be added to the range for error reporting.
+     * @return A {@link Placeholder} object representing the parsed placeholder.
+     * @throws IllegalArgumentException If the prefix is not matched.
+     * @throws SyntaxError If the placeholder is malformed or if matching braces are not closed.
+     */
+    static Placeholder parse(String stream, int startIdx, int offset) {
+      if (!PropertyPlaceholder.matchPrefix(stream, startIdx)) {
+        throw new IllegalArgumentException();
+      }
+
+      ParseStates state = new Expr();
+      int startExpr = startIdx + 2;
+
+      for (int idx = startExpr; idx < stream.length(); idx++) {
+        char c = stream.charAt(idx);
+        if (state instanceof Expr e && (c == '}' || c == ':')) {
+          String expr = stream.substring(startExpr, idx).trim();
+          if (c == '}') {
+            return new Placeholder(offset, new Range(startIdx, idx + 1), expr, null);
+          } else {
+            state = new DefaultValue(0, expr, idx + 1);
+          }
+        } else if (state instanceof DefaultValue d && c == '}' && d.nestingLevel == 0) {
+          return new Placeholder(offset, new Range(startIdx, idx + 1), d.expr, stream.substring(d.startDefault, idx).trim());
+        } else if (state instanceof DefaultValue d) {
+          if (c == '{') {
+            state = d.increaseNestingLevel();
+          } else if (c == '}') {
+            state = d.decreaseNestingLevel();
+          }
+        }
+      }
 
-    var contentsParser = getContentsParser(stripped);
-    if (contentsParser != null) {
-      var endIndex = parseDelimitersAndContents(stripped, 1, startColumn + 2, contentsParser);
-      return endIndex == segment.stripTrailing().length();
+      Range range = new Range(startIdx, stream.length());
+      throw new SyntaxError("Add missing '}' for this property placeholder or SpEL expression.", range.addOffset(offset));
     }
-    return true;
-  }
 
-  private static ObjIntConsumer<String> getContentsParser(String contents) {
-    if (contents.startsWith("${")) {
-      return SpelExpressionCheck::parseValidPropertyPlaceholder;
+    sealed interface ParseStates {
     }
-    if (contents.startsWith("#{")) {
-      return SpelExpressionCheck::parseValidSpelExpression;
+    record Expr() implements ParseStates {
     }
-    return null;
-  }
+    record DefaultValue(int nestingLevel, String expr, int startDefault) implements ParseStates {
+      DefaultValue increaseNestingLevel() {
+        return new DefaultValue(nestingLevel + 1, expr, startDefault);
+      }
 
-  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);
+      DefaultValue decreaseNestingLevel() {
+        return new DefaultValue(nestingLevel - 1, expr, startDefault);
+      }
     }
   }
-
-  private static boolean isValidSpelExpression(String expressionString) {
-    expressionString = expressionString.strip();
-    if (expressionString.isEmpty()) {
-      return false;
+  private static class SpEL {
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: romainbrenguier
body: add a new line before
created_at: 2025-05-06T07:27:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
 
-  private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
+      public Placeholder {
+        if (!PROPERTY_PLACEHOLDER_PATTERN.asMatchPredicate().test(expr)) {
+          throw new SyntaxError("Correct this malformed property placeholder.", range.addOffset(offset));
+        }
+      }
 
-    if (stripped.startsWith("#{")) {
-      parseDelimitersAndContents(stripped, 1, startColumn + 2, SpelExpressionCheck::parseValidSpelExpression);
-      return true;
-    } else {
-      return PROPERTY_PLACEHOLDER_PATTERN.matcher(stripped).matches();
+      public String evaluate(String stream) {
+        char before = range.start > 0 ? stream.charAt(range.start - 1) : ' ';
+        if (before == '@') {
+          return "bean";
+        }
+        return "#var";
+      }
     }
-  }
 
-  private static boolean isValidPropertyPlaceholderDefaultSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    static boolean matchPrefix(String stream, int idx) {
+      return idx + 1 < stream.length() && stream.charAt(idx) == prefix && stream.charAt(idx + 1) == '{';
+    }
+
+    /**
+     * Parses a property placeholder from the given string starting at the specified index.
+     * Assumes that the prefix for a property placeholder is already matched.
+     *
+     * @param stream The input string containing the property placeholder.
+     * @param startIdx The starting index of the placeholder in the string.
+     * @param offset The offset to be added to the range for error reporting.
+     * @return A {@link Placeholder} object representing the parsed placeholder.
+     * @throws IllegalArgumentException If the prefix is not matched.
+     * @throws SyntaxError If the placeholder is malformed or if matching braces are not closed.
+     */
+    static Placeholder parse(String stream, int startIdx, int offset) {
+      if (!PropertyPlaceholder.matchPrefix(stream, startIdx)) {
+        throw new IllegalArgumentException();
+      }
+
+      ParseStates state = new Expr();
+      int startExpr = startIdx + 2;
+
+      for (int idx = startExpr; idx < stream.length(); idx++) {
+        char c = stream.charAt(idx);
+        if (state instanceof Expr e && (c == '}' || c == ':')) {
+          String expr = stream.substring(startExpr, idx).trim();
+          if (c == '}') {
+            return new Placeholder(offset, new Range(startIdx, idx + 1), expr, null);
+          } else {
+            state = new DefaultValue(0, expr, idx + 1);
+          }
+        } else if (state instanceof DefaultValue d && c == '}' && d.nestingLevel == 0) {
+          return new Placeholder(offset, new Range(startIdx, idx + 1), d.expr, stream.substring(d.startDefault, idx).trim());
+        } else if (state instanceof DefaultValue d) {
+          if (c == '{') {
+            state = d.increaseNestingLevel();
+          } else if (c == '}') {
+            state = d.decreaseNestingLevel();
+          }
+        }
+      }
 
-    var contentsParser = getContentsParser(stripped);
-    if (contentsParser != null) {
-      var endIndex = parseDelimitersAndContents(stripped, 1, startColumn + 2, contentsParser);
-      return endIndex == segment.stripTrailing().length();
+      Range range = new Range(startIdx, stream.length());
+      throw new SyntaxError("Add missing '}' for this property placeholder or SpEL expression.", range.addOffset(offset));
     }
-    return true;
-  }
 
-  private static ObjIntConsumer<String> getContentsParser(String contents) {
-    if (contents.startsWith("${")) {
-      return SpelExpressionCheck::parseValidPropertyPlaceholder;
+    sealed interface ParseStates {
     }
-    if (contents.startsWith("#{")) {
-      return SpelExpressionCheck::parseValidSpelExpression;
+    record Expr() implements ParseStates {
     }
-    return null;
-  }
+    record DefaultValue(int nestingLevel, String expr, int startDefault) implements ParseStates {
+      DefaultValue increaseNestingLevel() {
+        return new DefaultValue(nestingLevel + 1, expr, startDefault);
+      }
 
-  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);
+      DefaultValue decreaseNestingLevel() {
+        return new DefaultValue(nestingLevel - 1, expr, startDefault);
+      }
     }
   }
-
-  private static boolean isValidSpelExpression(String expressionString) {
-    expressionString = expressionString.strip();
-    if (expressionString.isEmpty()) {
-      return false;
+  private static class SpEL {
+    private static char prefix = '#';
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: romainbrenguier
body: final
created_at: 2025-05-06T07:27:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
 
-  private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-06T08:42:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-06T08:43:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
 
-  private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
+      public Placeholder {
+        if (!PROPERTY_PLACEHOLDER_PATTERN.asMatchPredicate().test(expr)) {
+          throw new SyntaxError("Correct this malformed property placeholder.", range.addOffset(offset));
+        }
+      }
 
-    if (stripped.startsWith("#{")) {
-      parseDelimitersAndContents(stripped, 1, startColumn + 2, SpelExpressionCheck::parseValidSpelExpression);
-      return true;
-    } else {
-      return PROPERTY_PLACEHOLDER_PATTERN.matcher(stripped).matches();
+      public String evaluate(String stream) {
+        char before = range.start > 0 ? stream.charAt(range.start - 1) : ' ';
+        if (before == '@') {
+          return "bean";
+        }
+        return "#var";
+      }
     }
-  }
 
-  private static boolean isValidPropertyPlaceholderDefaultSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    static boolean matchPrefix(String stream, int idx) {
+      return idx + 1 < stream.length() && stream.charAt(idx) == prefix && stream.charAt(idx + 1) == '{';
+    }
+
+    /**
+     * Parses a property placeholder from the given string starting at the specified index.
+     * Assumes that the prefix for a property placeholder is already matched.
+     *
+     * @param stream The input string containing the property placeholder.
+     * @param startIdx The starting index of the placeholder in the string.
+     * @param offset The offset to be added to the range for error reporting.
+     * @return A {@link Placeholder} object representing the parsed placeholder.
+     * @throws IllegalArgumentException If the prefix is not matched.
+     * @throws SyntaxError If the placeholder is malformed or if matching braces are not closed.
+     */
+    static Placeholder parse(String stream, int startIdx, int offset) {
+      if (!PropertyPlaceholder.matchPrefix(stream, startIdx)) {
+        throw new IllegalArgumentException();
+      }
+
+      ParseStates state = new Expr();
+      int startExpr = startIdx + 2;
+
+      for (int idx = startExpr; idx < stream.length(); idx++) {
+        char c = stream.charAt(idx);
+        if (state instanceof Expr e && (c == '}' || c == ':')) {
+          String expr = stream.substring(startExpr, idx).trim();
+          if (c == '}') {
+            return new Placeholder(offset, new Range(startIdx, idx + 1), expr, null);
+          } else {
+            state = new DefaultValue(0, expr, idx + 1);
+          }
+        } else if (state instanceof DefaultValue d && c == '}' && d.nestingLevel == 0) {
+          return new Placeholder(offset, new Range(startIdx, idx + 1), d.expr, stream.substring(d.startDefault, idx).trim());
+        } else if (state instanceof DefaultValue d) {
+          if (c == '{') {
+            state = d.increaseNestingLevel();
+          } else if (c == '}') {
+            state = d.decreaseNestingLevel();
+          }
+        }
+      }
 
-    var contentsParser = getContentsParser(stripped);
-    if (contentsParser != null) {
-      var endIndex = parseDelimitersAndContents(stripped, 1, startColumn + 2, contentsParser);
-      return endIndex == segment.stripTrailing().length();
+      Range range = new Range(startIdx, stream.length());
+      throw new SyntaxError("Add missing '}' for this property placeholder or SpEL expression.", range.addOffset(offset));
     }
-    return true;
-  }
 
-  private static ObjIntConsumer<String> getContentsParser(String contents) {
-    if (contents.startsWith("${")) {
-      return SpelExpressionCheck::parseValidPropertyPlaceholder;
+    sealed interface ParseStates {
     }
-    if (contents.startsWith("#{")) {
-      return SpelExpressionCheck::parseValidSpelExpression;
+    record Expr() implements ParseStates {
     }
-    return null;
-  }
+    record DefaultValue(int nestingLevel, String expr, int startDefault) implements ParseStates {
+      DefaultValue increaseNestingLevel() {
+        return new DefaultValue(nestingLevel + 1, expr, startDefault);
+      }
 
-  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);
+      DefaultValue decreaseNestingLevel() {
+        return new DefaultValue(nestingLevel - 1, expr, startDefault);
+      }
     }
   }
-
-  private static boolean isValidSpelExpression(String expressionString) {
-    expressionString = expressionString.strip();
-    if (expressionString.isEmpty()) {
-      return false;
+  private static class SpEL {
+    private static char prefix = '#';
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-06T08:43:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-06T08:50:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
 
-  private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
+      public Placeholder {
+        if (!PROPERTY_PLACEHOLDER_PATTERN.asMatchPredicate().test(expr)) {
+          throw new SyntaxError("Correct this malformed property placeholder.", range.addOffset(offset));
+        }
+      }
 
-    if (stripped.startsWith("#{")) {
-      parseDelimitersAndContents(stripped, 1, startColumn + 2, SpelExpressionCheck::parseValidSpelExpression);
-      return true;
-    } else {
-      return PROPERTY_PLACEHOLDER_PATTERN.matcher(stripped).matches();
+      public String evaluate(String stream) {
+        char before = range.start > 0 ? stream.charAt(range.start - 1) : ' ';
+        if (before == '@') {
+          return "bean";
+        }
+        return "#var";
+      }
     }
-  }
 
-  private static boolean isValidPropertyPlaceholderDefaultSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    static boolean matchPrefix(String stream, int idx) {
+      return idx + 1 < stream.length() && stream.charAt(idx) == prefix && stream.charAt(idx + 1) == '{';
+    }
+
+    /**
+     * Parses a property placeholder from the given string starting at the specified index.
+     * Assumes that the prefix for a property placeholder is already matched.
+     *
+     * @param stream The input string containing the property placeholder.
+     * @param startIdx The starting index of the placeholder in the string.
+     * @param offset The offset to be added to the range for error reporting.
+     * @return A {@link Placeholder} object representing the parsed placeholder.
+     * @throws IllegalArgumentException If the prefix is not matched.
+     * @throws SyntaxError If the placeholder is malformed or if matching braces are not closed.
+     */
+    static Placeholder parse(String stream, int startIdx, int offset) {
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-06T08:52:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
 
-  private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
+      public Placeholder {
+        if (!PROPERTY_PLACEHOLDER_PATTERN.asMatchPredicate().test(expr)) {
+          throw new SyntaxError("Correct this malformed property placeholder.", range.addOffset(offset));
+        }
+      }
 
-    if (stripped.startsWith("#{")) {
-      parseDelimitersAndContents(stripped, 1, startColumn + 2, SpelExpressionCheck::parseValidSpelExpression);
-      return true;
-    } else {
-      return PROPERTY_PLACEHOLDER_PATTERN.matcher(stripped).matches();
+      public String evaluate(String stream) {
+        char before = range.start > 0 ? stream.charAt(range.start - 1) : ' ';
+        if (before == '@') {
+          return "bean";
+        }
+        return "#var";
+      }
     }
-  }
 
-  private static boolean isValidPropertyPlaceholderDefaultSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    static boolean matchPrefix(String stream, int idx) {
+      return idx + 1 < stream.length() && stream.charAt(idx) == prefix && stream.charAt(idx + 1) == '{';
+    }
+
+    /**
+     * Parses a property placeholder from the given string starting at the specified index.
+     * Assumes that the prefix for a property placeholder is already matched.
+     *
+     * @param stream The input string containing the property placeholder.
+     * @param startIdx The starting index of the placeholder in the string.
+     * @param offset The offset to be added to the range for error reporting.
+     * @return A {@link Placeholder} object representing the parsed placeholder.
+     * @throws IllegalArgumentException If the prefix is not matched.
+     * @throws SyntaxError If the placeholder is malformed or if matching braces are not closed.
+     */
+    static Placeholder parse(String stream, int startIdx, int offset) {
+      if (!PropertyPlaceholder.matchPrefix(stream, startIdx)) {
+        throw new IllegalArgumentException();
+      }
+
+      ParseStates state = new Expr();
+      int startExpr = startIdx + 2;
+
+      for (int idx = startExpr; idx < stream.length(); idx++) {
+        char c = stream.charAt(idx);
+        if (state instanceof Expr e && (c == '}' || c == ':')) {
+          String expr = stream.substring(startExpr, idx).trim();
+          if (c == '}') {
+            return new Placeholder(offset, new Range(startIdx, idx + 1), expr, null);
+          } else {
+            state = new DefaultValue(0, expr, idx + 1);
+          }
+        } else if (state instanceof DefaultValue d && c == '}' && d.nestingLevel == 0) {
+          return new Placeholder(offset, new Range(startIdx, idx + 1), d.expr, stream.substring(d.startDefault, idx).trim());
+        } else if (state instanceof DefaultValue d) {
+          if (c == '{') {
+            state = d.increaseNestingLevel();
+          } else if (c == '}') {
+            state = d.decreaseNestingLevel();
+          }
+        }
+      }
 
-    var contentsParser = getContentsParser(stripped);
-    if (contentsParser != null) {
-      var endIndex = parseDelimitersAndContents(stripped, 1, startColumn + 2, contentsParser);
-      return endIndex == segment.stripTrailing().length();
+      Range range = new Range(startIdx, stream.length());
+      throw new SyntaxError("Add missing '}' for this property placeholder or SpEL expression.", range.addOffset(offset));
     }
-    return true;
-  }
 
-  private static ObjIntConsumer<String> getContentsParser(String contents) {
-    if (contents.startsWith("${")) {
-      return SpelExpressionCheck::parseValidPropertyPlaceholder;
+    sealed interface ParseStates {
     }
-    if (contents.startsWith("#{")) {
-      return SpelExpressionCheck::parseValidSpelExpression;
+    record Expr() implements ParseStates {
     }
-    return null;
-  }
+    record DefaultValue(int nestingLevel, String expr, int startDefault) implements ParseStates {
+      DefaultValue increaseNestingLevel() {
+        return new DefaultValue(nestingLevel + 1, expr, startDefault);
+      }
 
-  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);
+      DefaultValue decreaseNestingLevel() {
+        return new DefaultValue(nestingLevel - 1, expr, startDefault);
+      }
     }
   }
-
-  private static boolean isValidSpelExpression(String expressionString) {
-    expressionString = expressionString.strip();
-    if (expressionString.isEmpty()) {
-      return false;
+  private static class SpEL {
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-06T08:52:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
 
-  private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
+      public Placeholder {
+        if (!PROPERTY_PLACEHOLDER_PATTERN.asMatchPredicate().test(expr)) {
+          throw new SyntaxError("Correct this malformed property placeholder.", range.addOffset(offset));
+        }
+      }
 
-    if (stripped.startsWith("#{")) {
-      parseDelimitersAndContents(stripped, 1, startColumn + 2, SpelExpressionCheck::parseValidSpelExpression);
-      return true;
-    } else {
-      return PROPERTY_PLACEHOLDER_PATTERN.matcher(stripped).matches();
+      public String evaluate(String stream) {
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-06T09:01:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,162 +149,182 @@ private void reportIssue(Tree expression, SyntaxError error) {
     }
   }
 
-  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;
+  private static void checkStringContents(String content, int offset) throws SyntaxError {
+    int idx = 0;
+    while (idx < content.length()) {
+      if (PropertyPlaceholder.matchPrefix(content, idx)) {
+        var placeholder = PropertyPlaceholder.parse(content, idx, offset);
+        idx = placeholder.range.end;
+      } else if (SpEL.matchPrefix(content, idx)) {
+        var expr = SpEL.parse(content, idx, offset);
+        idx = expr.range.end;
+      } else {
+        ++idx;
       }
     }
   }
 
-  /**
-   * Parses the following grammatical expression, starting at <code>startIndex</code> in `value`:
-   *
-   * <pre>
-   * ('{' contents '}')?
-   * </pre>
-   * <p>
-   * Where correct bracing is checked and then <code>contents</code> is parsed using the given <code>parseContents</code> function.
-   *
-   * @param value         string containing the character sequence to parse
-   * @param startIndex    index of the opening delimiter we start from in <code>value</code>
-   * @param startColumn   offset with the position of <code>value</code> within a potentially longer original string (used for reporting)
-   * @param parseContents function to parse <code>contents</code>
-   * @throws SyntaxError when the input does not comply with the expected grammatical expression
-   */
-  private static int parseDelimitersAndContents(
-    String value,
-    int startIndex,
-    int startColumn,
-    ObjIntConsumer<String> 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 class SyntaxError extends RuntimeException {
 
-  private static int parseDelimiterBraces(String value, int startIndex, int startColumn) throws SyntaxError {
-    if (value.charAt(startIndex) != '{') {
-      return startIndex;
+    SyntaxError(String message, Range range) {
+      super(message);
+      this.range = range;
     }
 
-    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;
-        }
+    public final Range range;
+  }
+  private record Range(int start, int end) {
+    public Range {
+      if (start >= end) {
+        throw new IllegalArgumentException();
       }
     }
 
-    // +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);
+    public Range addOffset(int offset) {
+      return new Range(start + offset, end + offset);
     }
   }
 
-  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 class PropertyPlaceholder {
+    private static char prefix = '$';
 
-  private static boolean isValidPropertyPlaceholderFirstSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
+      public Placeholder {
+        if (!PROPERTY_PLACEHOLDER_PATTERN.asMatchPredicate().test(expr)) {
+          throw new SyntaxError("Correct this malformed property placeholder.", range.addOffset(offset));
+        }
+      }
 
-    if (stripped.startsWith("#{")) {
-      parseDelimitersAndContents(stripped, 1, startColumn + 2, SpelExpressionCheck::parseValidSpelExpression);
-      return true;
-    } else {
-      return PROPERTY_PLACEHOLDER_PATTERN.matcher(stripped).matches();
+      public String evaluate(String stream) {
+        char before = range.start > 0 ? stream.charAt(range.start - 1) : ' ';
+        if (before == '@') {
+          return "bean";
+        }
+        return "#var";
+      }
     }
-  }
 
-  private static boolean isValidPropertyPlaceholderDefaultSegment(String segment, int startColumn) throws SyntaxError {
-    var stripped = segment.stripLeading();
-    startColumn += segment.length() - stripped.length();
-    stripped = stripped.stripTrailing();
+    static boolean matchPrefix(String stream, int idx) {
+      return idx + 1 < stream.length() && stream.charAt(idx) == prefix && stream.charAt(idx + 1) == '{';
+    }
+
+    /**
+     * Parses a property placeholder from the given string starting at the specified index.
+     * Assumes that the prefix for a property placeholder is already matched.
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: erwan-serandour
body: :+1: 
created_at: 2025-05-06T09:10:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -185,70 +195,66 @@ public Range addOffset(int offset) {
     }
   }
 
-  private static class PropertyPlaceholder {
-    private static char prefix = '$';
-
-    record Placeholder(int offset, Range range, String expr, @Nullable String defaultValue) {
-      public Placeholder {
-        if (!PROPERTY_PLACEHOLDER_PATTERN.asMatchPredicate().test(expr)) {
-          throw new SyntaxError("Correct this malformed property placeholder.", range.addOffset(offset));
-        }
-      }
+  private record ParseCtx(String expressionSource, int offset) {
path: java-checks/src/main/java/org/sonar/java/checks/spring/SpelExpressionCheck.java
user: login: romainbrenguier
body: add a bit of doc here, in particular what's the offset
created_at: 2025-05-06T11:47:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqube-next[bot]
created_at: 2025-05-06T12:10:39Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://next.sonarqube.com/sonarqube/dashboard?id=org.sonarsource.java%3Ajava&pullRequest=5128) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.java%3Ajava&pullRequest=5128&resolved=false) [0 New issues](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.java%3Ajava&pullRequest=5128&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.java%3Ajava&fixedInPullRequest=5128) [0 Fixed issues](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.java%3Ajava&fixedInPullRequest=5128)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.java%3Ajava&pullRequest=5128&issueStatuses=ACCEPTED) [1 Accepted issue](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.java%3Ajava&pullRequest=5128&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/security_hotspots?id=org.sonarsource.java%3Ajava&pullRequest=5128) [0 Security Hotspots](https://next.sonarqube.com/sonarqube/security_hotspots?id=org.sonarsource.java%3Ajava&pullRequest=5128)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.java%3Ajava&pullRequest=5128&metric=new_coverage&view=list) [95.5% Coverage on New Code](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.java%3Ajava&pullRequest=5128&metric=new_coverage&view=list)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.java%3Ajava&pullRequest=5128&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.java%3Ajava&pullRequest=5128&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://next.sonarqube.com/sonarqube/dashboard?id=org.sonarsource.java%3Ajava&pullRequest=5128)


author_association: NONE