001    /*
002     * Copyright 2010-2015 JetBrains s.r.o.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    
017    package org.jetbrains.kotlin.resolve.calls;
018    
019    import com.intellij.lang.ASTNode;
020    import com.intellij.psi.PsiElement;
021    import com.intellij.psi.util.PsiTreeUtil;
022    import org.jetbrains.annotations.NotNull;
023    import org.jetbrains.annotations.Nullable;
024    import org.jetbrains.kotlin.builtins.KotlinBuiltIns;
025    import org.jetbrains.kotlin.descriptors.*;
026    import org.jetbrains.kotlin.lexer.JetTokens;
027    import org.jetbrains.kotlin.psi.*;
028    import org.jetbrains.kotlin.resolve.BindingContext;
029    import org.jetbrains.kotlin.resolve.DescriptorUtils;
030    import org.jetbrains.kotlin.resolve.calls.callUtil.CallUtilPackage;
031    import org.jetbrains.kotlin.resolve.calls.context.BasicCallResolutionContext;
032    import org.jetbrains.kotlin.resolve.calls.context.CheckValueArgumentsMode;
033    import org.jetbrains.kotlin.resolve.calls.context.ResolutionContext;
034    import org.jetbrains.kotlin.resolve.calls.context.TemporaryTraceAndCache;
035    import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall;
036    import org.jetbrains.kotlin.resolve.calls.results.OverloadResolutionResults;
037    import org.jetbrains.kotlin.resolve.calls.results.OverloadResolutionResultsUtil;
038    import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowInfo;
039    import org.jetbrains.kotlin.resolve.calls.util.CallMaker;
040    import org.jetbrains.kotlin.resolve.calls.util.FakeCallableDescriptorForObject;
041    import org.jetbrains.kotlin.resolve.constants.CompileTimeConstant;
042    import org.jetbrains.kotlin.resolve.constants.IntegerValueConstant;
043    import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator;
044    import org.jetbrains.kotlin.resolve.scopes.receivers.*;
045    import org.jetbrains.kotlin.types.ErrorUtils;
046    import org.jetbrains.kotlin.types.JetType;
047    import org.jetbrains.kotlin.types.TypeUtils;
048    import org.jetbrains.kotlin.types.expressions.*;
049    import org.jetbrains.kotlin.types.expressions.typeInfoFactory.TypeInfoFactoryPackage;
050    
051    import javax.inject.Inject;
052    import java.util.Collections;
053    import java.util.List;
054    
055    import static org.jetbrains.kotlin.diagnostics.Errors.*;
056    import static org.jetbrains.kotlin.resolve.calls.context.ContextDependency.INDEPENDENT;
057    import static org.jetbrains.kotlin.resolve.scopes.receivers.ReceiversPackage.createQualifier;
058    import static org.jetbrains.kotlin.resolve.scopes.receivers.ReceiversPackage.resolveAsStandaloneExpression;
059    import static org.jetbrains.kotlin.types.TypeUtils.NO_EXPECTED_TYPE;
060    
061    public class CallExpressionResolver {
062    
063        private final CallResolver callResolver;
064        private final KotlinBuiltIns builtIns;
065    
066        public CallExpressionResolver(@NotNull CallResolver callResolver, @NotNull KotlinBuiltIns builtIns) {
067            this.callResolver = callResolver;
068            this.builtIns = builtIns;
069        }
070    
071        private ExpressionTypingServices expressionTypingServices;
072    
073        @Inject
074        public void setExpressionTypingServices(@NotNull ExpressionTypingServices expressionTypingServices) {
075            this.expressionTypingServices = expressionTypingServices;
076        }
077    
078        @Nullable
079        public ResolvedCall<FunctionDescriptor> getResolvedCallForFunction(
080                @NotNull Call call, @NotNull JetExpression callExpression,
081                @NotNull ResolutionContext context, @NotNull CheckValueArgumentsMode checkArguments,
082                @NotNull boolean[] result
083        ) {
084            OverloadResolutionResults<FunctionDescriptor> results = callResolver.resolveFunctionCall(
085                    BasicCallResolutionContext.create(context, call, checkArguments));
086            if (!results.isNothing()) {
087                result[0] = true;
088                return OverloadResolutionResultsUtil.getResultingCall(results, context.contextDependency);
089            }
090            result[0] = false;
091            return null;
092        }
093    
094        @Nullable
095        private JetType getVariableType(
096                @NotNull JetSimpleNameExpression nameExpression, @NotNull ReceiverValue receiver,
097                @Nullable ASTNode callOperationNode, @NotNull ExpressionTypingContext context, @NotNull boolean[] result
098        ) {
099            TemporaryTraceAndCache temporaryForVariable = TemporaryTraceAndCache.create(
100                    context, "trace to resolve as local variable or property", nameExpression);
101            Call call = CallMaker.makePropertyCall(receiver, callOperationNode, nameExpression);
102            BasicCallResolutionContext contextForVariable = BasicCallResolutionContext.create(
103                    context.replaceTraceAndCache(temporaryForVariable),
104                    call, CheckValueArgumentsMode.ENABLED);
105            OverloadResolutionResults<VariableDescriptor> resolutionResult = callResolver.resolveSimpleProperty(contextForVariable);
106    
107            // if the expression is a receiver in a qualified expression, it should be resolved after the selector is resolved
108            boolean isLHSOfDot = JetPsiUtil.isLHSOfDot(nameExpression);
109            if (!resolutionResult.isNothing()) {
110                boolean isQualifier = isLHSOfDot && resolutionResult.isSingleResult()
111                                      && resolutionResult.getResultingDescriptor() instanceof FakeCallableDescriptorForObject;
112                if (!isQualifier) {
113                    result[0] = true;
114                    temporaryForVariable.commit();
115                    return resolutionResult.isSingleResult() ? resolutionResult.getResultingDescriptor().getReturnType() : null;
116                }
117            }
118    
119            QualifierReceiver qualifier = createQualifier(nameExpression, receiver, context);
120            if (qualifier != null) {
121                result[0] = true;
122                if (!isLHSOfDot) {
123                    resolveAsStandaloneExpression(qualifier, context);
124                }
125                return null;
126            }
127            temporaryForVariable.commit();
128            result[0] = !resolutionResult.isNothing();
129            return resolutionResult.isSingleResult() ? resolutionResult.getResultingDescriptor().getReturnType() : null;
130        }
131    
132        @NotNull
133        public JetTypeInfo getSimpleNameExpressionTypeInfo(
134                @NotNull JetSimpleNameExpression nameExpression, @NotNull ReceiverValue receiver,
135                @Nullable ASTNode callOperationNode, @NotNull ExpressionTypingContext context
136        ) {
137            boolean[] result = new boolean[1];
138    
139            TemporaryTraceAndCache temporaryForVariable = TemporaryTraceAndCache.create(
140                    context, "trace to resolve as variable", nameExpression);
141            JetType type =
142                    getVariableType(nameExpression, receiver, callOperationNode, context.replaceTraceAndCache(temporaryForVariable), result);
143            // TODO: for a safe call, it's necessary to set receiver != null here, as inside ArgumentTypeResolver.analyzeArgumentsAndRecordTypes
144            // Unfortunately it provokes problems with x?.y!!.foo() with the following x!!.bar():
145            // x != null proceeds to successive statements
146    
147            if (result[0]) {
148                temporaryForVariable.commit();
149                return TypeInfoFactoryPackage.createTypeInfo(type, context);
150            }
151    
152            Call call = CallMaker.makeCall(nameExpression, receiver, callOperationNode, nameExpression, Collections.<ValueArgument>emptyList());
153            TemporaryTraceAndCache temporaryForFunction = TemporaryTraceAndCache.create(
154                    context, "trace to resolve as function", nameExpression);
155            ResolutionContext newContext = context.replaceTraceAndCache(temporaryForFunction);
156            ResolvedCall<FunctionDescriptor> resolvedCall = getResolvedCallForFunction(
157                    call, nameExpression, newContext, CheckValueArgumentsMode.ENABLED, result);
158            if (result[0]) {
159                FunctionDescriptor functionDescriptor = resolvedCall != null ? resolvedCall.getResultingDescriptor() : null;
160                temporaryForFunction.commit();
161                boolean hasValueParameters = functionDescriptor == null || functionDescriptor.getValueParameters().size() > 0;
162                context.trace.report(FUNCTION_CALL_EXPECTED.on(nameExpression, nameExpression, hasValueParameters));
163                type = functionDescriptor != null ? functionDescriptor.getReturnType() : null;
164                return TypeInfoFactoryPackage.createTypeInfo(type, context);
165            }
166    
167            temporaryForVariable.commit();
168            return TypeInfoFactoryPackage.noTypeInfo(context);
169        }
170    
171        @NotNull
172        public JetTypeInfo getCallExpressionTypeInfo(
173                @NotNull JetCallExpression callExpression, @NotNull ReceiverValue receiver,
174                @Nullable ASTNode callOperationNode, @NotNull ExpressionTypingContext context
175        ) {
176            JetTypeInfo typeInfo = getCallExpressionTypeInfoWithoutFinalTypeCheck(callExpression, receiver, callOperationNode, context);
177            if (context.contextDependency == INDEPENDENT) {
178                DataFlowUtils.checkType(typeInfo.getType(), callExpression, context);
179            }
180            return typeInfo;
181        }
182    
183        /**
184         * Visits a call expression and its arguments.
185         * Determines the result type and data flow information after the call.
186         */
187        @NotNull
188        public JetTypeInfo getCallExpressionTypeInfoWithoutFinalTypeCheck(
189                @NotNull JetCallExpression callExpression, @NotNull ReceiverValue receiver,
190                @Nullable ASTNode callOperationNode, @NotNull ExpressionTypingContext context
191        ) {
192            boolean[] result = new boolean[1];
193            Call call = CallMaker.makeCall(receiver, callOperationNode, callExpression);
194    
195            TemporaryTraceAndCache temporaryForFunction = TemporaryTraceAndCache.create(
196                    context, "trace to resolve as function call", callExpression);
197            ResolvedCall<FunctionDescriptor> resolvedCall = getResolvedCallForFunction(
198                    call, callExpression,
199                    // It's possible start of a call so we should reset safe call chain
200                    context.replaceTraceAndCache(temporaryForFunction).replaceInsideCallChain(false),
201                    CheckValueArgumentsMode.ENABLED, result);
202            if (result[0]) {
203                FunctionDescriptor functionDescriptor = resolvedCall != null ? resolvedCall.getResultingDescriptor() : null;
204                temporaryForFunction.commit();
205                if (callExpression.getValueArgumentList() == null && callExpression.getFunctionLiteralArguments().isEmpty()) {
206                    // there are only type arguments
207                    boolean hasValueParameters = functionDescriptor == null || functionDescriptor.getValueParameters().size() > 0;
208                    context.trace.report(FUNCTION_CALL_EXPECTED.on(callExpression, callExpression, hasValueParameters));
209                }
210                if (functionDescriptor == null) {
211                    return TypeInfoFactoryPackage.noTypeInfo(context);
212                }
213                if (functionDescriptor instanceof ConstructorDescriptor) {
214                    if (DescriptorUtils.isAnnotationClass(functionDescriptor.getContainingDeclaration())
215                        && !canInstantiateAnnotationClass(callExpression)) {
216                        context.trace.report(ANNOTATION_CLASS_CONSTRUCTOR_CALL.on(callExpression));
217                    }
218                    if (DescriptorUtils.isEnumClass(functionDescriptor.getContainingDeclaration())) {
219                        context.trace.report(ENUM_CLASS_CONSTRUCTOR_CALL.on(callExpression));
220                    }
221                }
222    
223                JetType type = functionDescriptor.getReturnType();
224                // Extracting jump out possible and jump point flow info from arguments, if any
225                List<? extends ValueArgument> arguments = callExpression.getValueArguments();
226                DataFlowInfo resultFlowInfo = resolvedCall.getDataFlowInfoForArguments().getResultInfo();
227                DataFlowInfo jumpFlowInfo = resultFlowInfo;
228                boolean jumpOutPossible = false;
229                for (ValueArgument argument: arguments) {
230                    JetTypeInfo argTypeInfo = context.trace.get(BindingContext.EXPRESSION_TYPE_INFO, argument.getArgumentExpression());
231                    if (argTypeInfo != null && argTypeInfo.getJumpOutPossible()) {
232                        jumpOutPossible = true;
233                        jumpFlowInfo = argTypeInfo.getJumpFlowInfo();
234                        break;
235                    }
236                }
237                return TypeInfoFactoryPackage.createTypeInfo(type, resultFlowInfo, jumpOutPossible, jumpFlowInfo);
238            }
239    
240            JetExpression calleeExpression = callExpression.getCalleeExpression();
241            if (calleeExpression instanceof JetSimpleNameExpression && callExpression.getTypeArgumentList() == null) {
242                TemporaryTraceAndCache temporaryForVariable = TemporaryTraceAndCache.create(
243                        context, "trace to resolve as variable with 'invoke' call", callExpression);
244                JetType type = getVariableType((JetSimpleNameExpression) calleeExpression, receiver, callOperationNode,
245                                               context.replaceTraceAndCache(temporaryForVariable), result);
246                Qualifier qualifier = temporaryForVariable.trace.get(BindingContext.QUALIFIER, calleeExpression);
247                if (result[0] && (qualifier == null || qualifier.getPackageView() == null)) {
248                    temporaryForVariable.commit();
249                    context.trace.report(FUNCTION_EXPECTED.on(calleeExpression, calleeExpression,
250                                                              type != null ? type : ErrorUtils.createErrorType("")));
251                    return TypeInfoFactoryPackage.noTypeInfo(context);
252                }
253            }
254            temporaryForFunction.commit();
255            return TypeInfoFactoryPackage.noTypeInfo(context);
256        }
257    
258        private static boolean canInstantiateAnnotationClass(@NotNull JetCallExpression expression) {
259            //noinspection unchecked
260            PsiElement parent = PsiTreeUtil.getParentOfType(expression, JetValueArgument.class, JetParameter.class);
261            if (parent instanceof JetValueArgument) {
262                return PsiTreeUtil.getParentOfType(parent, JetAnnotationEntry.class) != null;
263            }
264            else if (parent instanceof JetParameter) {
265                JetClass jetClass = PsiTreeUtil.getParentOfType(parent, JetClass.class);
266                if (jetClass != null) {
267                    return jetClass.hasModifier(JetTokens.ANNOTATION_KEYWORD);
268                }
269            }
270            return false;
271        }
272    
273        @NotNull
274        private JetTypeInfo getSelectorReturnTypeInfo(
275                @NotNull ReceiverValue receiver,
276                @Nullable ASTNode callOperationNode,
277                @Nullable JetExpression selectorExpression,
278                @NotNull ExpressionTypingContext context
279        ) {
280            if (selectorExpression instanceof JetCallExpression) {
281                return getCallExpressionTypeInfoWithoutFinalTypeCheck((JetCallExpression) selectorExpression, receiver,
282                                                                      callOperationNode, context);
283            }
284            else if (selectorExpression instanceof JetSimpleNameExpression) {
285                return getSimpleNameExpressionTypeInfo((JetSimpleNameExpression) selectorExpression, receiver, callOperationNode, context);
286            }
287            else if (selectorExpression != null) {
288                context.trace.report(ILLEGAL_SELECTOR.on(selectorExpression, selectorExpression.getText()));
289            }
290            return TypeInfoFactoryPackage.noTypeInfo(context);
291        }
292    
293        /**
294         * Extended variant of JetTypeInfo stores additional information
295         * about data flow info from the left-more receiver, e.g. x != null for
296         * foo(x!!)?.bar(y!!)?.baz()
297         */
298        private static class JetTypeInfoInsideSafeCall extends JetTypeInfo {
299    
300            private final DataFlowInfo safeCallChainInfo;
301    
302            private JetTypeInfoInsideSafeCall(@NotNull JetTypeInfo typeInfo, @Nullable DataFlowInfo safeCallChainInfo) {
303                super(typeInfo.getType(), typeInfo.getDataFlowInfo(), typeInfo.getJumpOutPossible(), typeInfo.getJumpFlowInfo());
304                this.safeCallChainInfo = safeCallChainInfo;
305            }
306    
307            /**
308             * Returns safe call chain information which is taken from the left-most receiver of a chain
309             * foo(x!!)?.bar(y!!)?.gav() ==> x != null is safe call chain information
310             */
311            @Nullable
312            public DataFlowInfo getSafeCallChainInfo() {
313                return safeCallChainInfo;
314            }
315        }
316    
317    
318        /**
319         * Visits a qualified expression like x.y or x?.z controlling data flow information changes.
320         *
321         * @return qualified expression type together with data flow information
322         */
323        @NotNull
324        public JetTypeInfo getQualifiedExpressionTypeInfo(
325                @NotNull JetQualifiedExpression expression, @NotNull ExpressionTypingContext context
326        ) {
327            // TODO : functions as values
328            JetExpression selectorExpression = expression.getSelectorExpression();
329            JetExpression receiverExpression = expression.getReceiverExpression();
330            boolean safeCall = (expression.getOperationSign() == JetTokens.SAFE_ACCESS);
331            ResolutionContext contextForReceiver = context.replaceExpectedType(NO_EXPECTED_TYPE).
332                    replaceContextDependency(INDEPENDENT).
333                    replaceInsideCallChain(true); // Enter call chain
334            // Visit receiver (x in x.y or x?.z) here. Recursion is possible.
335            JetTypeInfo receiverTypeInfo = expressionTypingServices.getTypeInfo(receiverExpression, contextForReceiver);
336            JetType receiverType = receiverTypeInfo.getType();
337            QualifierReceiver qualifierReceiver = (QualifierReceiver) context.trace.get(BindingContext.QUALIFIER, receiverExpression);
338    
339            if (receiverType == null) receiverType = ErrorUtils.createErrorType("Type for " + expression.getText());
340    
341            ReceiverValue receiver = qualifierReceiver == null ? new ExpressionReceiver(receiverExpression, receiverType) : qualifierReceiver;
342            DataFlowInfo receiverDataFlowInfo = receiverTypeInfo.getDataFlowInfo();
343            // Receiver changes should be always applied, at least for argument analysis
344            context = context.replaceDataFlowInfo(receiverDataFlowInfo);
345    
346            // Visit selector (y in x.y) here. Recursion is also possible.
347            JetTypeInfo selectorReturnTypeInfo = getSelectorReturnTypeInfo(
348                    receiver, expression.getOperationTokenNode(), selectorExpression, context);
349            JetType selectorReturnType = selectorReturnTypeInfo.getType();
350    
351            resolveDeferredReceiverInQualifiedExpression(qualifierReceiver, expression, context);
352            checkNestedClassAccess(expression, context);
353    
354            //TODO move further
355            if (safeCall) {
356                if (selectorReturnType != null && !KotlinBuiltIns.isUnit(selectorReturnType)) {
357                    if (TypeUtils.isNullableType(receiverType)) {
358                        selectorReturnType = TypeUtils.makeNullable(selectorReturnType);
359                        selectorReturnTypeInfo = selectorReturnTypeInfo.replaceType(selectorReturnType);
360                    }
361                }
362            }
363            // TODO : this is suspicious: remove this code?
364            if (selectorExpression != null && selectorReturnType != null) {
365                context.trace.recordType(selectorExpression, selectorReturnType);
366            }
367    
368            CompileTimeConstant<?> value = ConstantExpressionEvaluator.evaluate(expression, context.trace, context.expectedType);
369            if (value instanceof IntegerValueConstant && ((IntegerValueConstant) value).isPure()) {
370                return ExpressionTypingUtils.createCompileTimeConstantTypeInfo(value, expression, context, builtIns);
371            }
372    
373            JetTypeInfo typeInfo;
374            DataFlowInfo safeCallChainInfo;
375            if (receiverTypeInfo instanceof JetTypeInfoInsideSafeCall) {
376                safeCallChainInfo = ((JetTypeInfoInsideSafeCall) receiverTypeInfo).getSafeCallChainInfo();
377            }
378            else {
379                safeCallChainInfo = null;
380            }
381            if (safeCall) {
382                if (safeCallChainInfo == null) safeCallChainInfo = receiverDataFlowInfo;
383                if (context.insideCallChain) {
384                    // If we are inside safe call chain, we SHOULD take arguments into account, for example
385                    // x?.foo(y!!)?.bar(x.field)?.gav(y.field) (like smartCasts\safecalls\longChain)
386                    // Also, we should provide further safe call chain data flow information or
387                    // if we are in the most left safe call then just take it from receiver
388                    typeInfo = new JetTypeInfoInsideSafeCall(selectorReturnTypeInfo, safeCallChainInfo);
389                }
390                else {
391                    // Here we should not take selector data flow info into account because it's only one branch, see KT-7204
392                    // x?.foo(y!!) // y becomes not-nullable during argument analysis
393                    // y.bar()     // ERROR: y is nullable at this point
394                    // So we should just take safe call chain data flow information, e.g. foo(x!!)?.bar()?.gav()
395                    // If it's null, we must take receiver normal info, it's a chain with length of one like foo(x!!)?.bar() and
396                    // safe call chain information does not yet exist
397                    typeInfo = selectorReturnTypeInfo.replaceDataFlowInfo(safeCallChainInfo);
398                }
399            }
400            else {
401                // It's not a safe call, so we can take selector data flow information
402                // Safe call chain information also should be provided because it's can be a part of safe call chain
403                if (context.insideCallChain || safeCallChainInfo == null) {
404                    // Not a safe call inside call chain with safe calls OR
405                    // call chain without safe calls at all
406                    typeInfo = new JetTypeInfoInsideSafeCall(selectorReturnTypeInfo, selectorReturnTypeInfo.getDataFlowInfo());
407                }
408                else {
409                    // Exiting call chain with safe calls -- take data flow info from the lest-most receiver
410                    // foo(x!!)?.bar().gav()
411                    typeInfo = selectorReturnTypeInfo.replaceDataFlowInfo(safeCallChainInfo);
412                }
413            }
414            if (context.contextDependency == INDEPENDENT) {
415                DataFlowUtils.checkType(typeInfo.getType(), expression, context);
416            }
417            return typeInfo;
418        }
419    
420        private static void resolveDeferredReceiverInQualifiedExpression(
421                @Nullable QualifierReceiver qualifierReceiver,
422                @NotNull JetQualifiedExpression qualifiedExpression,
423                @NotNull ExpressionTypingContext context
424        ) {
425            if (qualifierReceiver == null) return;
426            JetExpression calleeExpression =
427                    JetPsiUtil.deparenthesize(CallUtilPackage.getCalleeExpressionIfAny(qualifiedExpression.getSelectorExpression()), false);
428            DeclarationDescriptor selectorDescriptor =
429                    calleeExpression instanceof JetReferenceExpression
430                    ? context.trace.get(BindingContext.REFERENCE_TARGET, (JetReferenceExpression) calleeExpression) : null;
431            ReceiversPackage.resolveAsReceiverInQualifiedExpression(qualifierReceiver, context, selectorDescriptor);
432        }
433    
434        private static void checkNestedClassAccess(
435                @NotNull JetQualifiedExpression expression,
436                @NotNull ExpressionTypingContext context
437        ) {
438            JetExpression selectorExpression = expression.getSelectorExpression();
439            if (selectorExpression == null) return;
440    
441            // A.B - if B is a nested class accessed by outer class, 'A' and 'A.B' were marked as qualifiers
442            // a.B - if B is a nested class accessed by instance reference, 'a.B' was marked as a qualifier, but 'a' was not (it's an expression)
443    
444            Qualifier expressionQualifier = context.trace.get(BindingContext.QUALIFIER, expression);
445            Qualifier receiverQualifier = context.trace.get(BindingContext.QUALIFIER, expression.getReceiverExpression());
446    
447            if (receiverQualifier == null && expressionQualifier != null) {
448                assert expressionQualifier.getClassifier() instanceof ClassDescriptor :
449                        "Only class can (package cannot) be accessed by instance reference: " + expressionQualifier;
450                context.trace.report(NESTED_CLASS_ACCESSED_VIA_INSTANCE_REFERENCE
451                                             .on(selectorExpression, (ClassDescriptor) expressionQualifier.getClassifier()));
452            }
453        }
454    }