diff --git a/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinRecursiveCallLineMarkerProvider.kt.172 b/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinRecursiveCallLineMarkerProvider.kt.172 new file mode 100644 index 00000000000..231be8d8fc5 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinRecursiveCallLineMarkerProvider.kt.172 @@ -0,0 +1,174 @@ +/* + * Copyright 2010-2015 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 org.jetbrains.kotlin.idea.highlighter + +import com.intellij.codeHighlighting.Pass +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.LineMarkerProvider +import com.intellij.icons.AllIcons +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.openapi.progress.ProgressManager +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.inspections.RecursivePropertyAccessorInspection +import org.jetbrains.kotlin.idea.util.getReceiverTargetDescriptor +import org.jetbrains.kotlin.lexer.KtToken +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.parents +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.inline.InlineUtil +import org.jetbrains.kotlin.resolve.scopes.receivers.Receiver +import org.jetbrains.kotlin.resolve.scopes.receivers.ReceiverValue +import org.jetbrains.kotlin.types.expressions.OperatorConventions +import org.jetbrains.kotlin.util.OperatorNameConventions +import java.util.* + +class KotlinRecursiveCallLineMarkerProvider : LineMarkerProvider { + override fun getLineMarkerInfo(element: PsiElement) = null + + override fun collectSlowLineMarkers(elements: MutableList, result: MutableCollection>) { + val markedLineNumbers = HashSet() + + for (element in elements) { + ProgressManager.checkCanceled() + if (element is KtElement) { + val lineNumber = element.getLineNumber() + if (lineNumber !in markedLineNumbers && isRecursiveCall(element)) { + markedLineNumbers.add(lineNumber) + result.add(RecursiveMethodCallMarkerInfo(element)) + } + } + } + } + + private fun getEnclosingFunction(element: KtElement, stopOnNonInlinedLambdas: Boolean): KtNamedFunction? { + for (parent in element.parents) { + when (parent) { + is KtFunctionLiteral -> if (stopOnNonInlinedLambdas && !InlineUtil.isInlinedArgument(parent, parent.analyze(), false)) return null + is KtNamedFunction -> { + when (parent.parent) { + is KtBlockExpression, is KtClassBody, is KtFile, is KtScript -> return parent + else -> if (stopOnNonInlinedLambdas && !InlineUtil.isInlinedArgument(parent, parent.analyze(), false)) return null + } + } + is KtClassOrObject -> return null + } + } + return null + } + + private fun isRecursiveCall(element: KtElement): Boolean { + if (RecursivePropertyAccessorInspection.isRecursivePropertyAccess(element)) return true + if (RecursivePropertyAccessorInspection.isRecursiveSyntheticPropertyAccess(element)) return true + // Fast check for names without resolve + val resolveName = getCallNameFromPsi(element) ?: return false + val enclosingFunction = getEnclosingFunction(element, false) ?: return false + + val enclosingFunctionName = enclosingFunction.name + if (enclosingFunctionName != OperatorNameConventions.INVOKE.asString() + && enclosingFunctionName != resolveName.asString()) return false + + // Check that there were no not-inlined lambdas on the way to enclosing function + if (enclosingFunction != getEnclosingFunction(element, true)) return false + + val bindingContext = element.analyze() + val enclosingFunctionDescriptor = bindingContext[BindingContext.FUNCTION, enclosingFunction] ?: return false + + val call = bindingContext[BindingContext.CALL, element] ?: return false + val resolvedCall = bindingContext[BindingContext.RESOLVED_CALL, call] ?: return false + + if (resolvedCall.candidateDescriptor.original != enclosingFunctionDescriptor) return false + + fun isDifferentReceiver(receiver: Receiver?): Boolean { + if (receiver !is ReceiverValue) return false + + val receiverOwner = receiver.getReceiverTargetDescriptor(bindingContext) ?: return true + + return when (receiverOwner) { + is SimpleFunctionDescriptor -> receiverOwner != enclosingFunctionDescriptor + is ClassDescriptor -> receiverOwner != enclosingFunctionDescriptor.containingDeclaration + else -> return true + } + } + + if (isDifferentReceiver(resolvedCall.dispatchReceiver)) return false + return true + } + + private class RecursiveMethodCallMarkerInfo(callElement: KtElement) + : LineMarkerInfo( + callElement, + callElement.textRange, + AllIcons.Gutter.RecursiveMethod, + Pass.LINE_MARKERS, + { "Recursive call" }, + null, + GutterIconRenderer.Alignment.RIGHT + ) { + + override fun createGutterRenderer(): GutterIconRenderer? { + return object : LineMarkerInfo.LineMarkerGutterIconRenderer(this) { + override fun getClickAction() = null // to place breakpoint on mouse click + } + } + } + +} + +private fun PsiElement.getLineNumber(): Int { + return PsiDocumentManager.getInstance(project).getDocument(containingFile)!!.getLineNumber(textOffset) +} + +private fun getCallNameFromPsi(element: KtElement): Name? { + when (element) { + is KtSimpleNameExpression -> { + val elementParent = element.getParent() + when (elementParent) { + is KtCallExpression -> return Name.identifier(element.getText()) + is KtOperationExpression -> { + val operationReference = elementParent.operationReference + if (element == operationReference) { + val node = operationReference.getReferencedNameElementType() + return if (node is KtToken) { + val conventionName = if (elementParent is KtPrefixExpression) + OperatorConventions.getNameForOperationSymbol(node, true, false) + else + OperatorConventions.getNameForOperationSymbol(node) + + conventionName ?: Name.identifier(element.getText()) + } + else { + Name.identifier(element.getText()) + } + } + } + } + } + is KtArrayAccessExpression -> + return OperatorNameConventions.GET + is KtThisExpression -> + if (element.getParent() is KtCallExpression) { + return OperatorNameConventions.INVOKE + } + } + + return null +} diff --git a/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinSuspendCallLineMarkerProvider.kt.172 b/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinSuspendCallLineMarkerProvider.kt.172 new file mode 100644 index 00000000000..aa413b3cd83 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinSuspendCallLineMarkerProvider.kt.172 @@ -0,0 +1,114 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.highlighter + +import com.intellij.codeHighlighting.Pass +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.LineMarkerProvider +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.openapi.progress.ProgressManager +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.PropertyDescriptor +import org.jetbrains.kotlin.descriptors.VariableDescriptorWithAccessors +import org.jetbrains.kotlin.descriptors.accessors +import org.jetbrains.kotlin.idea.KotlinIcons +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.refactoring.getLineNumber +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.BindingContext.* +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.calls.checkers.isBuiltInCoroutineContext +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode + +class KotlinSuspendCallLineMarkerProvider : LineMarkerProvider { + private class SuspendCallMarkerInfo(callElement: KtElement, message: String) : LineMarkerInfo( + callElement, + callElement.textRange, + KotlinIcons.SUSPEND_CALL, + Pass.LINE_MARKERS, + { message }, + null, + GutterIconRenderer.Alignment.RIGHT + ) { + override fun createGutterRenderer(): GutterIconRenderer? { + return object : LineMarkerInfo.LineMarkerGutterIconRenderer(this) { + override fun getClickAction(): AnAction? = null + } + } + } + + override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? = null + + override fun collectSlowLineMarkers( + elements: MutableList, + result: MutableCollection> + ) { + val markedLineNumbers = HashSet() + + for (element in elements) { + ProgressManager.checkCanceled() + + if (element !is KtExpression) continue + val lineNumber = element.getLineNumber() + if (lineNumber in markedLineNumbers) continue + if (!element.hasSuspendCalls()) continue + + markedLineNumbers += lineNumber + result += if (element is KtForExpression) { + SuspendCallMarkerInfo(element.loopRange!!, "Suspending iteration") + } else { + SuspendCallMarkerInfo(element, "Suspend function call") + } + } + } +} + +private fun KtExpression.isValidCandidateExpression(): Boolean { + if (this is KtOperationReferenceExpression || this is KtForExpression || this is KtProperty || this is KtNameReferenceExpression) return true + val parent = parent + if (parent is KtCallExpression && parent.calleeExpression == this) return true + return false +} + +fun KtExpression.hasSuspendCalls(bindingContext: BindingContext = analyze(BodyResolveMode.PARTIAL)): Boolean { + if (!isValidCandidateExpression()) return false + + return when (this) { + is KtForExpression -> { + val iteratorResolvedCall = bindingContext[LOOP_RANGE_ITERATOR_RESOLVED_CALL, loopRange] + val loopRangeHasNextResolvedCall = bindingContext[LOOP_RANGE_HAS_NEXT_RESOLVED_CALL, loopRange] + val loopRangeNextResolvedCall = bindingContext[LOOP_RANGE_NEXT_RESOLVED_CALL, loopRange] + listOf(iteratorResolvedCall, loopRangeHasNextResolvedCall, loopRangeNextResolvedCall).any { + it?.resultingDescriptor?.isSuspend == true + } + } + is KtProperty -> { + if (hasDelegateExpression()) { + val variableDescriptor = bindingContext[DECLARATION_TO_DESCRIPTOR, this] as? VariableDescriptorWithAccessors + val accessors = variableDescriptor?.accessors ?: emptyList() + accessors.any { accessor -> + val delegatedFunctionDescriptor = bindingContext[DELEGATED_PROPERTY_RESOLVED_CALL, accessor]?.resultingDescriptor + delegatedFunctionDescriptor?.isSuspend == true + } + } else { + false + } + } + else -> { + val resolvedCall = getResolvedCall(bindingContext) + if ((resolvedCall?.resultingDescriptor as? FunctionDescriptor)?.isSuspend == true) true + else { + val propertyDescriptor = resolvedCall?.resultingDescriptor as? PropertyDescriptor + val s = propertyDescriptor?.fqNameSafe?.asString() + s?.startsWith("kotlin.coroutines.") == true && s.endsWith(".coroutineContext") + } + } + } +} diff --git a/idea/testData/codeInsight/lineMarker/recursiveCall/conventionCall.kt.172 b/idea/testData/codeInsight/lineMarker/recursiveCall/conventionCall.kt.172 new file mode 100644 index 00000000000..573b62766e8 --- /dev/null +++ b/idea/testData/codeInsight/lineMarker/recursiveCall/conventionCall.kt.172 @@ -0,0 +1,52 @@ +operator fun Any.get(a: Int) { + if (a > 0) { + this[a - 1] + } +} + +class A { + override fun equals(other: Any?): Boolean { + this == other + return true + } + + operator fun inc(): A { + this++ + ++this + return this + } + + operator fun component1(): Int { + // TODO: should be recursion marker too + val (a) = this + return 1 + } + + operator fun unaryPlus() { + +this + } + + operator fun unaryMinus() { + -this + } + + operator fun plus(a: Int) { + this + 1 + this += 1 + } + + operator fun invoke() { + val a = A() + a() + a.invoke() + + this.invoke() + this() + } +} + +class B +operator fun B.invoke() { + this() + invoke() +} \ No newline at end of file