Repository: spring-projects/spring-framework (2026-03-27T05:32:39Z)

Issue:
`ProfilesParser` silently accepts unbalanced parentheses in profile expressions
`Profiles.of("dev)")` or `@Profile("dev)")` with unbalanced parentheses is silently accepted and treated as "dev".

## Reproduction
```java
@RestController
@Profile("dev)") // unbalanced parentheses
public class DevController {

    @GetMapping("/message")
    public String readDevMessage() {
        return "spring";
    }
}
```

```yml
spring:
  application:
    name: profile
  profiles:
    active: dev
```

```
$ curl http://localhost:8080/message
spring
```

```
2026-03-26T19:26:50.425+09:00 DEBUG 97890 --- [profile] [           main] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'devController'
```

The controller is loaded even though the profile expression is malformed.

### Additional cases
The same behavior occurs with:
- "dev)"
- "(dev"

Both are accepted without any exception.

## Observation
Unbalanced parentheses like `"dev)"` and `"(dev"` are accepted without any exception. They appear to be partially interpreted instead of being rejected.

From the implementation, it seems this behavior may be related to how `ProfilesParser.parseTokens()` handles closing parentheses at the top level, where unmatched ")" does not trigger a validation failure.

## Question
Is this behavior intentional?

- The current implementation already rejects standalone "(" or ")"
  - in `ProfileTests.malformedExpressions()`
- However, when combined with profile names, unbalanced parentheses are silently accepted

So it is unclear whether:
1. This is intended lenient parsing behavior, or
2. This is a missing validation case

## Additional note on tests

While reviewing existing tests, I noticed the following:

`ProfilesTests.java 150:160` 
```java
@Test
void ofAndExpression() {
    Profiles profiles = Profiles.of("(spring & framework)");
    assertAndExpression(profiles);
}

@Test
void ofAndExpressionWithoutSpaces() {
    Profiles profiles = Profiles.of("spring&framework)"); // here
    assertAndExpression(profiles);
}
```

Given the test name `ofAndExpressionWithoutSpaces`, I expected something like:

```java
Profiles.of("(spring&framework)")
```

so I'm wondering if the current test is intentionally covering lenient parsing of unbalanced parentheses, or if this might be unintended.

## Possible improvements

If this behavior is intentional, it might be helpful to clarify it in the documentation, as malformed expressions are currently accepted without any indication.

If not, it may be worth tightening validation in `ProfilesParser` to reject unbalanced parentheses consistently, and possibly updating related test cases as well.

I'd be happy to contribute a fix or improvements in either direction if this is something worth addressing.


Discussion:
Closing in favor of #36550.
Thanks @bebeis !



Patch:
diff --git a/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java
index 9d477c9d7d97..326f6f659c4e 100644
--- a/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java
+++ b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java
@@ -88,13 +88,10 @@ private static Profiles parseTokens(String expression, StringTokenizer tokens, C
 				}
 				case "!" -> elements.add(not(parseTokens(expression, tokens, Context.NEGATE)));
 				case ")" -> {
-					Profiles merged = merge(expression, elements, operator);
 					if (context == Context.PARENTHESIS) {
-						return merged;
+						return merge(expression, elements, operator);
 					}
-					elements.clear();
-					elements.add(merged);
-					operator = null;
+					assertWellFormed(expression, false);
 				}
 				default -> {
 					Profiles value = equals(token);
@@ -105,6 +102,7 @@ private static Profiles parseTokens(String expression, StringTokenizer tokens, C
 				}
 			}
 		}
+		assertWellFormed(expression, context != Context.PARENTHESIS);
 		return merge(expression, elements, operator);
 	}
 


Test patch:
diff --git a/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java b/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java
index dde5aa676a97..7cfcb837dceb 100644
--- a/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java
+++ b/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java
@@ -155,7 +155,7 @@ void ofAndExpression() {
 
 	@Test
 	void ofAndExpressionWithoutSpaces() {
-		Profiles profiles = Profiles.of("spring&framework)");
+		Profiles profiles = Profiles.of("(spring&framework)");
 		assertAndExpression(profiles);
 	}
 
@@ -292,6 +292,15 @@ void malformedExpressions() {
 		assertMalformed(() -> Profiles.of("a & b | c"));
 	}
 
+	@Test
+	void malformedExpressionsWithUnbalancedParentheses() {
+		assertMalformed(() -> Profiles.of("dev)"));
+		assertMalformed(() -> Profiles.of("(dev"));
+		assertMalformed(() -> Profiles.of("spring&framework)"));
+		assertMalformed(() -> Profiles.of("((dev)"));
+		assertMalformed(() -> Profiles.of("(dev))"));
+	}
+
 	@Test
 	void sensibleToString() {
 		assertThat(Profiles.of("spring")).hasToString("spring");