This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 24640b69fb2e526b257b5b3d05054f480efc311d 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 ae83893170..9caf78705f 100644 --- a/java/org/apache/el/parser/AstConcatenation.java +++ b/java/org/apache/el/parser/AstConcatenation.java @@ -40,6 +40,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 68c51a8782..be2921cbcc 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]
