This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit b366c9ce14120ffc5819be413d88fd37b4c52166
Author: opencode <[email protected]>
AuthorDate: Sun Oct 18 13:45:34 2026 +0200

    Report accurate result type for EL concatenation expressions
    
    AstConcatenation (the += operator) merges Maps, Sets and Lists
    in-place when the operand types match and only falls back to string
    concatenation otherwise, but getType() unconditionally reported
    String.class. Applications calling ValueExpression.getType() on a
    merge expression such as ${m1 += m2} were told the result was a
    String while getValue() returned the merged Map.
    
    getType() now evaluates both operands and mirrors the branch
    structure of getValue(), returning the left operand's concrete class
    for the merge branches and String.class for the string concatenation
    path. Unlike getValue() it performs no putAll()/addAll() mutations;
    evaluating the operands is required to determine the type, consistent
    with AstChoice, AstElvis, AstNullCoalescing and AstAssign getType()
    implementations and with the documented ValueExpression.getType()
    contract.
    
    Adds regression tests for the Map, Set and List merge branches and
    for the unchanged string path.
---
 java/org/apache/el/parser/AstConcatenation.java    | 19 ++++++++
 .../org/apache/el/parser/TestAstConcatenation.java | 51 ++++++++++++++++++++++
 2 files changed, 70 insertions(+)

diff --git a/java/org/apache/el/parser/AstConcatenation.java 
b/java/org/apache/el/parser/AstConcatenation.java
index 860e99fb11..74aeb47bf1 100644
--- a/java/org/apache/el/parser/AstConcatenation.java
+++ b/java/org/apache/el/parser/AstConcatenation.java
@@ -39,6 +39,25 @@ public class AstConcatenation extends SimpleNode {
 
     @Override
     public Class<?> getType(EvaluationContext ctx) throws ELException {
+        // Mirror the getValue type determination, but without performing any
+        // merging. The operands must be evaluated to determine the result 
type.
+        Object obj0 = this.children[0].getValue(ctx);
+        Object obj1 = this.children[1].getValue(ctx);
+
+        if (obj0 != null) {
+            if (obj0 instanceof Map<?, ?> && obj1 instanceof Map<?, ?>) {
+                // Maps are merged
+                return obj0.getClass();
+            } else if (obj0 instanceof Set<?> && obj1 instanceof 
Collection<?>) {
+                // If obj0 is a Set then merge
+                return obj0.getClass();
+            } else if (obj0 instanceof List<?> && obj1 instanceof 
Collection<?>) {
+                // If obj0 is a List then concatenate
+                return obj0.getClass();
+            }
+        }
+
+        // Otherwise the values are coerced to string and concatenated
         return String.class;
     }
 }
diff --git a/test/org/apache/el/parser/TestAstConcatenation.java 
b/test/org/apache/el/parser/TestAstConcatenation.java
index 8a28d865a2..4fc557789f 100644
--- a/test/org/apache/el/parser/TestAstConcatenation.java
+++ b/test/org/apache/el/parser/TestAstConcatenation.java
@@ -119,4 +119,55 @@ public class TestAstConcatenation {
         Assert.assertEquals(String.class, ve.getType(context));
         Assert.assertEquals("a3", ve.getValue(context));
     }
+
+    @Test
+    public void testGetTypeMapMerge() {
+        // The merge branches of += produce the left operand, not a string, so
+        // getType() must not report String for them.
+        ELProcessor processor = new ELProcessor();
+        ELContext context = processor.getELManager().getELContext();
+        ExpressionFactory factory = ELManager.getExpressionFactory();
+
+        ValueExpression ve = factory.createValueExpression(
+                context, "${{'a':'1'} += {'b':'2'}}", Object.class);
+
+        Assert.assertTrue(Map.class.isAssignableFrom(ve.getType(context)));
+    }
+
+    @Test
+    public void testGetTypeSetMerge() {
+        ELProcessor processor = new ELProcessor();
+        ELContext context = processor.getELManager().getELContext();
+        ExpressionFactory factory = ELManager.getExpressionFactory();
+
+        ValueExpression ve = factory.createValueExpression(
+                context, "${{'a','b'} += ['c']}", Object.class);
+
+        Assert.assertTrue(Set.class.isAssignableFrom(ve.getType(context)));
+    }
+
+    @Test
+    public void testGetTypeListMerge() {
+        ELProcessor processor = new ELProcessor();
+        ELContext context = processor.getELManager().getELContext();
+        ExpressionFactory factory = ELManager.getExpressionFactory();
+
+        ValueExpression ve = factory.createValueExpression(
+                context, "${['a','b'] += ['c']}", Object.class);
+
+        Assert.assertTrue(List.class.isAssignableFrom(ve.getType(context)));
+    }
+
+    @Test
+    public void testGetTypeStringConcatenation() {
+        // Non-collection operands still take the string concatenation path.
+        ELProcessor processor = new ELProcessor();
+        ELContext context = processor.getELManager().getELContext();
+        ExpressionFactory factory = ELManager.getExpressionFactory();
+
+        ValueExpression ve = factory.createValueExpression(
+                context, "${1 += 2}", Object.class);
+
+        Assert.assertEquals(String.class, ve.getType(context));
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to