From 4e28d6351604db49503ac6396093800f95574d64 Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Mon, 11 Sep 2017 17:21:40 +0700 Subject: [PATCH] StreamChainInliner: support comparators; support getters in lambdas/methodrefs --- .../dataFlow/DfaMemoryStateImpl.java | 58 +++--- .../dataFlow/StandardInstructionVisitor.java | 9 + .../dataFlow/inliner/ComparatorModel.java | 169 ++++++++++++++++++ .../dataFlow/inliner/StreamChainInliner.java | 67 ++++++- .../dataFlow/value/DfaExpressionFactory.java | 8 +- .../dataFlow/fixture/StreamInlining.java | 41 ++++- 6 files changed, 303 insertions(+), 49 deletions(-) create mode 100644 java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inliner/ComparatorModel.java diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaMemoryStateImpl.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaMemoryStateImpl.java index 0a1a828b70cf..ad60aab3cfff 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaMemoryStateImpl.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaMemoryStateImpl.java @@ -23,13 +23,15 @@ import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.UnorderedPair; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.psi.*; +import com.intellij.psi.PsiModifierListOwner; +import com.intellij.psi.PsiPrimitiveType; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiVariable; import com.intellij.psi.util.TypeConversionUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.Stack; -import com.siyeh.ig.psiutils.MethodUtils; import gnu.trove.*; import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; @@ -1027,9 +1029,10 @@ public class DfaMemoryStateImpl implements DfaMemoryState { private boolean isUnknownState(DfaValue val) { val = unwrap(val); if (val instanceof DfaVariableValue) { - if (myUnknownVariables.contains(val)) return true; - DfaVariableValue negatedValue = ((DfaVariableValue)val).getNegatedValue(); - if (negatedValue != null && myUnknownVariables.contains(negatedValue)) return true; + DfaVariableValue var = (DfaVariableValue)val; + if (myUnknownVariables.contains(val) || myUnknownVariables.contains(var.getNegatedValue())) return true; + return equivalentVariables(var) + .anyMatch(v -> myUnknownVariables.contains(v) || myUnknownVariables.contains(v.getNegatedValue())); } return false; } @@ -1097,23 +1100,6 @@ public class DfaMemoryStateImpl implements DfaMemoryState { return map.with(factType, factType.fromDfaValue(value)); } - @Nullable - private LongRangeSet getRange(DfaValue value) { - if (value instanceof DfaVariableValue) { - DfaVariableValue var = (DfaVariableValue)value; - if (var.getPsiVariable() instanceof PsiMethod && MethodUtils.isStringLength((PsiMethod)var.getPsiVariable())) { - DfaVariableValue qualifier = var.getQualifier(); - if(qualifier != null) { - DfaConstValue constValue = getConstantValue(qualifier); - if (constValue != null && constValue.getValue() instanceof String) { - return LongRangeSet.point(((String)constValue.getValue()).length()); - } - } - } - } - return null; - } - void setVariableState(DfaVariableValue dfaVar, DfaVariableState state) { assert !myUnknownVariables.contains(dfaVar); if (state.equals(myDefaultVariableStates.get(dfaVar))) { @@ -1124,26 +1110,24 @@ public class DfaMemoryStateImpl implements DfaMemoryState { myCachedHash = null; } + @NotNull + private StreamEx equivalentVariables(DfaVariableValue var) { + DfaVariableValue qualifier = var.getQualifier(); + if (qualifier == null) return StreamEx.empty(); + int qualifierIndex = getEqClassIndex(qualifier); + if (qualifierIndex == -1) return StreamEx.empty(); + return StreamEx.of(myEqClasses.get(qualifierIndex).getMemberValues()) + .without(qualifier).select(DfaVariableValue.class) + .map(eqQualifier -> getFactory().getVarFactory() + .createVariableValue(var.getPsiVariable(), var.getVariableType(), var.isNegated(), eqQualifier)); + } + private DfaVariableState findVariableState(DfaVariableValue var) { DfaVariableState state = myVariableStates.get(var); if (state != null) { return state; } - DfaVariableValue qualifier = var.getQualifier(); - if (qualifier == null) return null; - int qualifierIndex = getEqClassIndex(qualifier); - if (qualifierIndex == -1) return null; - for (DfaValue eqQualifier : myEqClasses.get(qualifierIndex).getMemberValues()) { - if (eqQualifier != qualifier && eqQualifier instanceof DfaVariableValue) { - DfaVariableValue eqValue = getFactory().getVarFactory() - .createVariableValue(var.getPsiVariable(), var.getVariableType(), var.isNegated(), (DfaVariableValue)eqQualifier); - state = myVariableStates.get(eqValue); - if (state != null) { - return state; - } - } - } - return null; + return equivalentVariables(var).map(myVariableStates::get).nonNull().findFirst().orElse(null); } @NotNull diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StandardInstructionVisitor.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StandardInstructionVisitor.java index bf5a01b4ffce..c25fd349dcd4 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StandardInstructionVisitor.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/StandardInstructionVisitor.java @@ -488,6 +488,15 @@ public class StandardInstructionVisitor extends InstructionVisitor { final PsiType type = instruction.getResultType(); final MethodCallInstruction.MethodType methodType = instruction.getMethodType(); + if (methodType == MethodCallInstruction.MethodType.METHOD_REFERENCE_CALL && qualifierValue instanceof DfaVariableValue) { + PsiMethod method = instruction.getTargetMethod(); + PsiModifierListOwner modifierListOwner = DfaExpressionFactory.getAccessedVariableOrGetter(method); + if (modifierListOwner != null) { + return factory.getVarFactory().createVariableValue(modifierListOwner, instruction.getResultType(), false, + (DfaVariableValue)qualifierValue); + } + } + if (methodType == MethodCallInstruction.MethodType.UNBOXING) { return factory.getBoxedFactory().createUnboxed(qualifierValue); } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inliner/ComparatorModel.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inliner/ComparatorModel.java new file mode 100644 index 000000000000..792b74f39894 --- /dev/null +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inliner/ComparatorModel.java @@ -0,0 +1,169 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInspection.dataFlow.inliner; + +import com.intellij.codeInspection.dataFlow.CFGBuilder; +import com.intellij.codeInspection.dataFlow.Nullness; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiUtil; +import com.intellij.util.ObjectUtils; +import com.siyeh.ig.callMatcher.CallMatcher; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.intellij.psi.CommonClassNames.JAVA_UTIL_COLLECTIONS; +import static com.intellij.psi.CommonClassNames.JAVA_UTIL_COMPARATOR; +import static com.siyeh.ig.callMatcher.CallMatcher.*; + +/** + * Simplified model for comparator: does not perform actual comparison, just executes key extractors, etc. + */ +abstract class ComparatorModel { + private static final CallMatcher KEY_EXTRACTOR = + anyOf(staticCall(JAVA_UTIL_COMPARATOR, "comparing", "comparingInt", "comparingLong", "comparingDouble").parameterCount(1), + staticCall(JAVA_UTIL_COMPARATOR, "comparing").parameterCount(2)); + private static final CallMatcher NULL_HOSTILE = anyOf(staticCall(JAVA_UTIL_COMPARATOR, "naturalOrder", "reverseOrder").parameterCount(0), + staticCall(JAVA_UTIL_COLLECTIONS, "reverseOrder").parameterCount(0)); + private static final CallMatcher NULL_FRIENDLY = staticCall(JAVA_UTIL_COMPARATOR, "nullsFirst", "nullsLast").parameterCount(1); + private static final CallMatcher REVERSED = instanceCall(JAVA_UTIL_COMPARATOR, "reversed").parameterCount(0); + private static final CallMatcher REVERSE_ORDER = staticCall(JAVA_UTIL_COLLECTIONS, "reverseOrder").parameterCount(1); + + private final boolean myFailsOnNull; + + protected ComparatorModel(boolean failsOnNull) { + myFailsOnNull = failsOnNull; + } + + abstract void evaluate(CFGBuilder builder); + + abstract void invoke(CFGBuilder builder); + + boolean failsOnNull() { + return myFailsOnNull; + } + + private static class NullHostile extends ComparatorModel { + NullHostile() { + super(true); + } + + @Override + void evaluate(CFGBuilder builder) {} + + @Override + void invoke(CFGBuilder builder) { + builder.pop(); + } + } + + private static class Unknown extends ComparatorModel { + private final PsiExpression myExpression; + + Unknown(PsiExpression expression) { + super(false); + myExpression = expression; + } + + @Override + void evaluate(CFGBuilder builder) { + builder.evaluateFunction(myExpression); + } + + @Override + void invoke(CFGBuilder builder) { + builder.pushUnknown().invokeFunction(2, myExpression).pop(); + } + } + + private static class NullFriendly extends ComparatorModel { + private final ComparatorModel myDownstream; + + NullFriendly(ComparatorModel downstream) { + super(false); + myDownstream = downstream; + } + + @Override + void evaluate(CFGBuilder builder) { + myDownstream.evaluate(builder); + } + + @Override + void invoke(CFGBuilder builder) { + builder.dup().ifNotNull().chain(myDownstream::invoke).elseBranch().pop().endIf(); + } + } + + private static class KeyExtractor extends ComparatorModel { + private final PsiExpression myKeyExtractor; + private final ComparatorModel myDownstream; + + private KeyExtractor(PsiExpression keyExtractor, ComparatorModel downstream) { + super(false); + myKeyExtractor = keyExtractor; + myDownstream = downstream; + } + + @Override + void evaluate(CFGBuilder builder) { + builder.evaluateFunction(myKeyExtractor); + myDownstream.evaluate(builder); + } + + @Override + void invoke(CFGBuilder builder) { + builder.invokeFunction(1, myKeyExtractor, myDownstream.myFailsOnNull ? Nullness.NOT_NULL : Nullness.UNKNOWN) + .chain(myDownstream::invoke); + } + } + + @NotNull + static ComparatorModel from(@Nullable PsiExpression expression) { + expression = PsiUtil.skipParenthesizedExprDown(expression); + if (expression == null || NULL_HOSTILE.matches(expression)) { + return new NullHostile(); + } + if (expression instanceof PsiReferenceExpression) { + PsiReferenceExpression ref = (PsiReferenceExpression)expression; + if ("CASE_INSENSITIVE_ORDER".equals(ref.getReferenceName())) { + PsiField field = ObjectUtils.tryCast(ref.resolve(), PsiField.class); + if (field != null && field.getContainingClass() != null && + CommonClassNames.JAVA_LANG_STRING.equals(field.getContainingClass().getQualifiedName())) { + return new NullHostile(); + } + } + } + PsiMethodCallExpression call = ObjectUtils.tryCast(expression, PsiMethodCallExpression.class); + if (call == null) return new Unknown(expression); + PsiExpression qualifier = call.getMethodExpression().getQualifierExpression(); + if (REVERSED.test(call) && qualifier != null) { + return from(qualifier); + } + if (REVERSE_ORDER.test(call)) { + return from(call.getArgumentList().getExpressions()[0]); + } + if (NULL_FRIENDLY.test(call) && qualifier != null) { + return new NullFriendly(from(qualifier)); + } + if (KEY_EXTRACTOR.test(call)) { + PsiExpression[] args = call.getArgumentList().getExpressions(); + PsiExpression keyExtractor = args[0]; + ComparatorModel downstream = args.length == 2 ? from(args[1]) : new NullHostile(); + return new KeyExtractor(keyExtractor, downstream); + } + return new Unknown(expression); + } +} diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inliner/StreamChainInliner.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inliner/StreamChainInliner.java index 14facf48367f..80fd027d5100 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inliner/StreamChainInliner.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inliner/StreamChainInliner.java @@ -45,12 +45,15 @@ public class StreamChainInliner implements CallInliner { private static final CallMatcher SUM_TERMINAL = instanceCall(JAVA_UTIL_STREAM_BASE_STREAM, "sum", "count").parameterCount(0); private static final CallMatcher OPTIONAL_TERMINAL = anyOf(instanceCall(JAVA_UTIL_STREAM_BASE_STREAM, "min", "max").parameterCount(0), - instanceCall(JAVA_UTIL_STREAM_BASE_STREAM, "min", "max", "reduce").parameterCount(1), + instanceCall(JAVA_UTIL_STREAM_BASE_STREAM, "reduce").parameterCount(1), instanceCall(JAVA_UTIL_STREAM_BASE_STREAM, "findFirst", "findAny").parameterCount(0)); + private static final CallMatcher MIN_MAX_TERMINAL = + instanceCall(JAVA_UTIL_STREAM_BASE_STREAM, "min", "max", "reduce").parameterCount(1); private static final CallMatcher SKIP_STEP = - instanceCall(JAVA_UTIL_STREAM_BASE_STREAM, "unordered", "parallel", "sequential", "sorted").parameterCount(0); - private static final CallMatcher SORTED = instanceCall(JAVA_UTIL_STREAM_STREAM, "sorted").parameterCount(1); + instanceCall(JAVA_UTIL_STREAM_BASE_STREAM, "unordered", "parallel", "sequential").parameterCount(0); + private static final CallMatcher SORTED = anyOf(instanceCall(JAVA_UTIL_STREAM_STREAM, "sorted").parameterCount(1), + instanceCall(JAVA_UTIL_STREAM_STREAM, "sorted").parameterCount(0)); private static final CallMatcher FILTER = instanceCall(JAVA_UTIL_STREAM_BASE_STREAM, "filter").parameterCount(1); private static final CallMatcher STATE_FILTER = anyOf(instanceCall(JAVA_UTIL_STREAM_BASE_STREAM, "distinct").parameterCount(0), instanceCall(JAVA_UTIL_STREAM_BASE_STREAM, "skip", "limit").parameterCount(1)); @@ -97,6 +100,7 @@ public class StreamChainInliner implements CallInliner { .register(FOR_TERMINAL, LambdaTerminalStep::new) .register(MATCH_TERMINAL, MatchTerminalStep::new) .register(SUM_TERMINAL, SumTerminalStep::new) + .register(MIN_MAX_TERMINAL, MinMaxTerminalStep::new) .register(OPTIONAL_TERMINAL, OptionalTerminalStep::new); static abstract class Step { @@ -130,6 +134,10 @@ public class StreamChainInliner implements CallInliner { .createTypeValue(myCall.getType(), DfaPsiUtil.getElementNullability(myCall.getType(), myCall.resolveMethod()))); } } + + boolean expectNotNull() { + return false; + } } static class UnknownTerminalStep extends Step { @@ -225,6 +233,37 @@ public class StreamChainInliner implements CallInliner { } } + static class MinMaxTerminalStep extends TerminalStep { + private final ComparatorModel myComparatorModel; + + MinMaxTerminalStep(@NotNull PsiMethodCallExpression call) { + super(call, null); + myComparatorModel = ComparatorModel.from(call.getArgumentList().getExpressions()[0]); + } + + @Override + protected void pushInitialValue(CFGBuilder builder) { + builder.push(builder.getFactory().getOptionalFactory().getOptional(false)); + } + + @Override + void before(CFGBuilder builder) { + myComparatorModel.evaluate(builder); + super.before(builder); + } + + @Override + void iteration(CFGBuilder builder) { + myComparatorModel.invoke(builder); + builder.pushVariable(myResult).push(builder.getFactory().getOptionalFactory().getOptional(true)).assign().pop(); + } + + @Override + boolean expectNotNull() { + return myComparatorModel.failsOnNull(); + } + } + static class MatchTerminalStep extends TerminalStep { MatchTerminalStep(@NotNull PsiMethodCallExpression call) { super(call, call.getArgumentList().getExpressions()[0]); @@ -273,7 +312,7 @@ public class StreamChainInliner implements CallInliner { @Override void iteration(CFGBuilder builder) { builder - .invokeFunction(1, myFunction) + .invokeFunction(1, myFunction, myNext.expectNotNull() ? Nullness.NOT_NULL : Nullness.UNKNOWN) .assignTo(builder.createTempVariable(StreamApiUtil.getStreamElementType(myCall.getType()))) .chain(myNext::iteration); } @@ -364,6 +403,11 @@ public class StreamChainInliner implements CallInliner { .pop() .chain(myNext::iteration); } + + @Override + boolean expectNotNull() { + return myNext.expectNotNull(); + } } static class StateFilterStep extends Step { @@ -391,24 +435,31 @@ public class StreamChainInliner implements CallInliner { } } - // Currently sorted is just a no-op as DFA results does not depend on sort order. - // In future we could check the comparator implementation - // (e.g. warn if stream can contain nulls, but comparator is not null-friendly) static class SortedStep extends Step { + private final ComparatorModel myComparatorModel; + SortedStep(@NotNull PsiMethodCallExpression call, Step next) { super(call, next, null); + myComparatorModel = ComparatorModel.from(ArrayUtil.getFirstElement(myCall.getArgumentList().getExpressions())); } @Override void before(CFGBuilder builder) { - builder.pushExpression(myCall.getArgumentList().getExpressions()[0]).pop(); + myComparatorModel.evaluate(builder); super.before(builder); } @Override void iteration(CFGBuilder builder) { + builder.dup(); + myComparatorModel.invoke(builder); myNext.iteration(builder); } + + @Override + boolean expectNotNull() { + return myComparatorModel.failsOnNull() || myNext.expectNotNull(); + } } static class BoxedStep extends Step { diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaExpressionFactory.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaExpressionFactory.java index e8b7d0a3d1b5..9be8a4e41092 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaExpressionFactory.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/value/DfaExpressionFactory.java @@ -28,10 +28,11 @@ import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.impl.JavaConstantExpressionEvaluator; import com.intellij.psi.impl.light.LightVariableBuilder; -import com.intellij.psi.util.PropertyUtil; +import com.intellij.psi.util.PropertyUtilBase; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtil; import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -181,14 +182,15 @@ public class DfaExpressionFactory { !DfaUtil.hasInitializationHacks((PsiField)var); } + @Contract("null -> null") @Nullable - private static PsiModifierListOwner getAccessedVariableOrGetter(final PsiElement target) { + public static PsiModifierListOwner getAccessedVariableOrGetter(final PsiElement target) { if (target instanceof PsiVariable) { return (PsiVariable)target; } if (target instanceof PsiMethod) { PsiMethod method = (PsiMethod)target; - if (PropertyUtil.isSimplePropertyGetter(method) && !(method.getReturnType() instanceof PsiPrimitiveType)) { + if (PropertyUtilBase.isSimplePropertyGetter(method) && !(method.getReturnType() instanceof PsiPrimitiveType)) { String qName = PsiUtil.getMemberQualifiedName(method); if (qName == null || !FALSE_GETTERS.value(qName)) { return method; diff --git a/java/java-tests/testData/inspection/dataFlow/fixture/StreamInlining.java b/java/java-tests/testData/inspection/dataFlow/fixture/StreamInlining.java index 0837632970a3..d48cf36358b6 100644 --- a/java/java-tests/testData/inspection/dataFlow/fixture/StreamInlining.java +++ b/java/java-tests/testData/inspection/dataFlow/fixture/StreamInlining.java @@ -36,8 +36,13 @@ public class StreamInlining { list.stream().filter(x -> x == null).distinct().limit(10).skip(1).filter(x -> x != null).forEach(System.out::println); } - class Holder { + static class Holder { Object obj; + @Nullable String nullable; + + @Nullable String getNullable() { + return nullable; + } } int hash(List holders) { @@ -131,4 +136,38 @@ public class StreamInlining { Stream.generate(() -> Optional.of("xyz")).filter(Optional::isPresent).forEach(System.out::println); LongStream.generate(() -> 5).limit(10).filter(x -> x > 6).forEach(s -> System.out.println(s)); } + + @Nullable String process(String s) { + return s.isEmpty() ? null : s; + } + + void testMinMax(List list) { + list.stream().map(this::process).max(Comparator.naturalOrder()); + list.stream().map(this::process).max(Comparator.nullsFirst(Comparator.naturalOrder())); + list.stream().map(this::process).max(Comparator.nullsFirst(Comparator.comparing(String::length))); + list.stream().map(this::process).min(Comparator.nullsLast(Comparator.comparing(String::length))); + list.stream().map(this::process).min(Comparator.comparing(String::length)); + list.stream().map(this::process).min(Comparator.comparing(String::length).reversed()); + } + + void testSorted(List list) { + list.stream().sorted(Comparator.comparing(this::process, Comparator.reverseOrder())).collect(Collectors.toList()); + list.stream().sorted(Comparator.comparing(this::process)).collect(Collectors.toList()); + list.stream().sorted(Comparator.comparing(this::process, Comparator.nullsFirst(Comparator.naturalOrder()))).collect(Collectors.toList()); + list.stream().map(this::process).sorted(Comparator.comparing(String::length)).collect(Collectors.toList()); + list.stream().map(this::process).sorted().collect(Collectors.toList()); + list.stream().map(this::process).sorted(String::compareToIgnoreCase).collect(Collectors.toList()); + list.stream().map(this::process).sorted(String.CASE_INSENSITIVE_ORDER).collect(Collectors.toList()); + } + + void testSortedCheck(List holders) { + holders.stream().sorted(Comparator.comparing(h -> h.nullable)).toArray(); + holders.stream().filter(h -> h.nullable != null).sorted(Comparator.comparing(h -> h.nullable)).toArray(); + + holders.stream().sorted(Comparator.comparing(h -> h.getNullable())).toArray(); + holders.stream().filter(h -> h.getNullable() != null).sorted(Comparator.comparing(h -> h.getNullable())).toArray(); + + holders.stream().sorted(Comparator.comparing(Holder::getNullable)).toArray(); + holders.stream().filter(h -> h.getNullable() != null).sorted(Comparator.comparing(Holder::getNullable)).toArray(); + } }