Skip to content

Commit 29f141b

Browse files
laiyichincopybara-github
authored andcommitted
Fix CEL Java sortBy macro expansion to preserve element type and avoid heterogeneous list literals.
PiperOrigin-RevId: 975736930
1 parent 9a97aec commit 29f141b

5 files changed

Lines changed: 149 additions & 42 deletions

File tree

extensions/src/main/java/dev/cel/extensions/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ java_library(
274274
"//common/ast",
275275
"//common/internal:comparison_functions",
276276
"//common/types",
277+
"//common/types:type_providers",
277278
"//compiler:compiler_builder",
278279
"//extensions:extension_library",
279280
"//parser:macro",

extensions/src/main/java/dev/cel/extensions/CelListsExtensions.java

Lines changed: 111 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -132,15 +132,17 @@ public enum Function {
132132
CelFunctionBinding.from("list_sort", Collection.class, CelListsExtensions::sort)),
133133
SORT_BY(
134134
CelFunctionDecl.newFunctionDeclaration(
135-
"lists.@sortByAssociatedKeys",
136-
CelOverloadDecl.newGlobalOverload(
135+
"@sortByAssociatedKeys",
136+
CelOverloadDecl.newMemberOverload(
137137
"list_sortByAssociatedKeys",
138-
"Sorts a list by a key value. Used by the 'sortBy' macro",
138+
"Sorts a list by an associated list of keys. Used by the 'sortBy' macro",
139139
ListType.create(TypeParamType.create("T")),
140-
ListType.create(TypeParamType.create("T")))),
140+
ListType.create(TypeParamType.create("T")),
141+
ListType.create(TypeParamType.create("U")))),
141142
CelFunctionBinding.from(
142143
"list_sortByAssociatedKeys",
143144
Collection.class,
145+
Collection.class,
144146
CelListsExtensions::sortByAssociatedKeys));
145147

146148
private final CelFunctionDecl functionDecl;
@@ -358,8 +360,18 @@ private static List<Object> reverse(Collection<Object> list) {
358360
}
359361
}
360362

363+
private static final CelObjectComparator OBJECT_COMPARATOR = new CelObjectComparator();
364+
361365
private static ImmutableList<Object> sort(Collection<Object> objects) {
362-
return ImmutableList.sortedCopyOf(new CelObjectComparator(), objects);
366+
if (objects.isEmpty()) {
367+
return ImmutableList.of();
368+
}
369+
if (objects.size() == 1) {
370+
Object single = objects.iterator().next();
371+
OBJECT_COMPARATOR.compare(single, single);
372+
return ImmutableList.copyOf(objects);
373+
}
374+
return ImmutableList.sortedCopyOf(OBJECT_COMPARATOR, objects);
363375
}
364376

365377
private static class CelObjectComparator implements Comparator<Object> {
@@ -383,6 +395,37 @@ public int compare(Object o1, Object o2) {
383395
}
384396
}
385397

398+
private static final String UNUSED_ITER_VAR = "#unused";
399+
private static final String SORT_BY_INPUT_VAR = "@__sortBy_input__";
400+
401+
/**
402+
* Expands the {@code list.sortBy(var, expr)} receiver macro into a binding expression that sorts
403+
* the target list using keys evaluated by mapping {@code expr} over each element.
404+
*
405+
* <p>For example, given:
406+
*
407+
* <pre>{@code
408+
* myList.sortBy(item, -item.field)
409+
* }</pre>
410+
*
411+
* <p>The macro expands into:
412+
*
413+
* <pre>{@code
414+
* cel.bind(@__sortBy_input__, myList,
415+
* @__sortBy_input__.@sortByAssociatedKeys(
416+
* @__sortBy_input__.map(item, -item.field)
417+
* )
418+
* )
419+
* }</pre>
420+
*
421+
* <p>Where:
422+
*
423+
* <ul>
424+
* <li>{@code @__sortBy_input__.map(item, -item.field)} evaluates the sort key for each element.
425+
* <li>{@code @sortByAssociatedKeys} stably sorts the input list elements based on their
426+
* corresponding sort keys.
427+
* </ul>
428+
*/
386429
private static Optional<CelExpr> sortByMacro(
387430
CelMacroExprFactory exprFactory, CelExpr target, ImmutableList<CelExpr> arguments) {
388431
checkNotNull(exprFactory);
@@ -400,56 +443,86 @@ private static Optional<CelExpr> sortByMacro(
400443
String varName = varIdent.ident().name();
401444
CelExpr sortKeyExpr = checkNotNull(arguments.get(1));
402445

403-
// Compute the key using the second argument of the `sortBy(e, key)` macro.
404-
// Combine the key and the value in a two-element list
405-
CelExpr step = exprFactory.newList(sortKeyExpr, varIdent);
406-
// Wrap the pair in another list in order to be able to use the `list+list` operator
407-
step = exprFactory.newList(step);
408-
// Append the key-value pair to the i
409-
step =
446+
// Build map comprehension: @__sortBy_input__.map(varName, sortKeyExpr)
447+
CelExpr targetIdent = exprFactory.newIdentifier(SORT_BY_INPUT_VAR);
448+
CelExpr mapStep =
410449
exprFactory.newGlobalCall(
411450
Operator.ADD.getFunction(),
412451
exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()),
413-
step);
414-
// Create an intermediate list and populate it with key-value pairs
415-
step =
452+
exprFactory.newList(sortKeyExpr));
453+
CelExpr mapCompr =
416454
exprFactory.fold(
417455
varName,
418-
target,
456+
targetIdent,
419457
exprFactory.getAccumulatorVarName(),
420458
exprFactory.newList(),
421-
exprFactory.newBoolLiteral(true), // Include all elements
422-
step,
459+
exprFactory.newBoolLiteral(true),
460+
mapStep,
423461
exprFactory.newIdentifier(exprFactory.getAccumulatorVarName()));
424-
// Finally, sort the list of key-value pairs and map it to a list of values
425-
step = exprFactory.newGlobalCall(Function.SORT_BY.getFunction(), step);
426462

427-
return Optional.of(step);
463+
// Build call: @__sortBy_input__.@sortByAssociatedKeys(mapCompr)
464+
CelExpr callExpr =
465+
exprFactory.newReceiverCall(
466+
Function.SORT_BY.getFunction(), exprFactory.newIdentifier(SORT_BY_INPUT_VAR), mapCompr);
467+
468+
// Build bind: cel.bind(@__sortBy_input__, target, callExpr)
469+
CelExpr bindExpr =
470+
exprFactory.fold(
471+
UNUSED_ITER_VAR,
472+
exprFactory.newList(),
473+
SORT_BY_INPUT_VAR,
474+
target,
475+
exprFactory.newBoolLiteral(false),
476+
exprFactory.newIdentifier(SORT_BY_INPUT_VAR),
477+
callExpr);
478+
479+
return Optional.of(bindExpr);
428480
}
429481

430-
@SuppressWarnings({"unchecked", "rawtypes"})
482+
/**
483+
* Sorts elements of {@code list} based on the natural order of corresponding elements in {@code
484+
* keys}.
485+
*
486+
* <p>Both {@code list} and {@code keys} must have the exact same size. The sorting is stable
487+
* (i.e., preserves the relative order of elements with equal keys).
488+
*
489+
* @param list The input list to sort
490+
* @param keys The associated keys evaluated for each element in {@code list}
491+
* @return A new {@link ImmutableList} containing the elements of {@code list} sorted by {@code
492+
* keys}
493+
*/
431494
private static ImmutableList<Object> sortByAssociatedKeys(
432-
Collection<List<Object>> keyValuePairs) {
433-
List<Object>[] array = keyValuePairs.toArray(new List[0]);
434-
Arrays.sort(array, new CelObjectByKeyComparator(new CelObjectComparator()));
435-
ImmutableList.Builder<Object> builder = ImmutableList.builderWithExpectedSize(array.length);
436-
for (List<Object> pair : array) {
437-
builder.add(pair.get(1));
495+
Collection<Object> list, Collection<Object> keys) {
496+
checkArgument(
497+
list.size() == keys.size(),
498+
"@sortByAssociatedKeys() expected a list of the same size as the associated keys"
499+
+ " list, but got %s in list and %s in keys",
500+
list.size(),
501+
keys.size());
502+
503+
int listSize = list.size();
504+
if (listSize == 0) {
505+
return ImmutableList.of();
438506
}
439-
return builder.build();
440-
}
441507

442-
private static class CelObjectByKeyComparator implements Comparator<Object> {
443-
private final CelObjectComparator keyComparator;
508+
Object[] listArray = list.toArray();
509+
Object[] keysArray = keys.toArray();
510+
if (listSize == 1) {
511+
OBJECT_COMPARATOR.compare(keysArray[0], keysArray[0]);
512+
return ImmutableList.copyOf(list);
513+
}
444514

445-
CelObjectByKeyComparator(CelObjectComparator keyComparator) {
446-
this.keyComparator = keyComparator;
515+
Integer[] indices = new Integer[listSize];
516+
for (int i = 0; i < listSize; i++) {
517+
indices[i] = i;
447518
}
448519

449-
@SuppressWarnings({"unchecked"})
450-
@Override
451-
public int compare(Object o1, Object o2) {
452-
return keyComparator.compare(((List<Object>) o1).get(0), ((List<Object>) o2).get(0));
520+
Arrays.sort(indices, (i1, i2) -> OBJECT_COMPARATOR.compare(keysArray[i1], keysArray[i2]));
521+
522+
ImmutableList.Builder<Object> builder = ImmutableList.builderWithExpectedSize(listSize);
523+
for (int index : indices) {
524+
builder.add(listArray[index]);
453525
}
526+
return builder.build();
454527
}
455528
}

extensions/src/test/java/dev/cel/extensions/BUILD.bazel

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ java_library(
4242
"//parser:unparser",
4343
"//runtime",
4444
"//runtime:function_binding",
45-
"//runtime:interpreter_util",
4645
"//runtime:lite_runtime",
4746
"//runtime:lite_runtime_factory",
4847
"//runtime:partial_vars",
4948
"//runtime:unknown_attributes",
5049
"//testing:cel_runtime_flavor",
50+
"//validator",
51+
"//validator:validator_builder",
52+
"//validator/validators:homogeneous_literal",
5153
"@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto",
5254
"@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto",
5355
"@cel_spec//proto/cel/expr/conformance/test:simple_java_proto",

extensions/src/test/java/dev/cel/extensions/CelExtensionsTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ public void getAllFunctionNames() {
185185
"distinct",
186186
"reverse",
187187
"sort",
188-
"lists.@sortByAssociatedKeys",
188+
"@sortByAssociatedKeys",
189189
"regex.replace",
190190
"regex.extract",
191191
"regex.extractAll",

extensions/src/test/java/dev/cel/extensions/CelListsExtensionsTest.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
2323
import com.google.testing.junit.testparameterinjector.TestParameters;
2424
import dev.cel.bundle.Cel;
25+
import dev.cel.common.CelAbstractSyntaxTree;
2526
import dev.cel.common.CelContainer;
2627
import dev.cel.common.CelValidationException;
2728
import dev.cel.common.CelValidationResult;
@@ -30,6 +31,9 @@
3031
import dev.cel.parser.CelStandardMacro;
3132
import dev.cel.runtime.CelEvaluationException;
3233
import dev.cel.testing.CelRuntimeFlavor;
34+
import dev.cel.validator.CelValidator;
35+
import dev.cel.validator.CelValidatorFactory;
36+
import dev.cel.validator.validators.HomogeneousLiteralValidator;
3337
import org.junit.Assume;
3438
import org.junit.Test;
3539
import org.junit.runner.RunWith;
@@ -64,7 +68,7 @@ public void functionList_byVersion() {
6468
"distinct",
6569
"reverse",
6670
"sort",
67-
"lists.@sortByAssociatedKeys");
71+
"@sortByAssociatedKeys");
6872
}
6973

7074
@Test
@@ -257,6 +261,9 @@ public void sort_success_heterogeneousNumbers(String expression, String expected
257261
@TestParameters(
258262
"{expression: '[SimpleTest{name: \"a\"}, SimpleTest{name: \"b\"}].sort()', "
259263
+ "expectedError: 'List elements must be comparable'}")
264+
@TestParameters(
265+
"{expression: '[SimpleTest{name: \"a\"}].sort()', "
266+
+ "expectedError: 'List elements must be comparable'}")
260267
public void sort_throws(String expression, String expectedError) throws Exception {
261268
assertThat(assertThrows(CelEvaluationException.class, () -> eval(cel, expression)))
262269
.hasCauseThat()
@@ -283,6 +290,11 @@ public void sort_throws(String expression, String expectedError) throws Exceptio
283290
+ "expected: '[SimpleTest{name: \"bar\"},"
284291
+ " SimpleTest{name: \"baz\"},"
285292
+ " SimpleTest{name: \"foo\"}]'}")
293+
@TestParameters(
294+
"{expression: '[SimpleTest{name: \"baz\"},"
295+
+ " SimpleTest{name: \"foo\"},"
296+
+ " SimpleTest{name: \"bar\"}].sortBy(e, e.name)[0].name', "
297+
+ "expected: '\"bar\"'}")
286298
public void sortBy_success(String expression, String expected) throws Exception {
287299
Object result = eval(cel, expression);
288300

@@ -311,6 +323,9 @@ public void sortBy_throws_validationException(String expression, String expected
311323
@TestParameters(
312324
"{expression: '[SimpleTest{name: \"a\"}, SimpleTest{name: \"b\"}].sortBy(e, e)', "
313325
+ "expectedError: 'List elements must be comparable'}")
326+
@TestParameters(
327+
"{expression: '[SimpleTest{name: \"a\"}].sortBy(e, e)', "
328+
+ "expectedError: 'List elements must be comparable'}")
314329
public void sortBy_throws_evaluationException(String expression, String expectedError)
315330
throws Exception {
316331
assertThat(assertThrows(CelEvaluationException.class, () -> eval(cel, expression)))
@@ -319,5 +334,21 @@ public void sortBy_throws_evaluationException(String expression, String expected
319334
.contains(expectedError);
320335
}
321336

322-
337+
@Test
338+
public void sortBy_withHomogeneousLiteralValidator_success() throws Exception {
339+
CelValidator validator =
340+
CelValidatorFactory.standardCelValidatorBuilder(cel)
341+
.addAstValidators(HomogeneousLiteralValidator.newInstance())
342+
.build();
343+
344+
CelAbstractSyntaxTree ast =
345+
cel.compile(
346+
"[SimpleTest{name: 'baz'}, SimpleTest{name: 'foo'}, SimpleTest{name: 'bar'}]"
347+
+ ".sortBy(e, e.name)[0].name")
348+
.getAst();
349+
CelValidationResult result = validator.validate(ast);
350+
351+
assertThat(result.hasError()).isFalse();
352+
assertThat(cel.createProgram(ast).eval()).isEqualTo("bar");
353+
}
323354
}

0 commit comments

Comments
 (0)