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]
