org.jetbrains.kotlin.idea.refactoring.JetRefactoringUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.jetbrains.kotlin.idea.refactoring.JetRefactoringUtil.java

Source

/*
 * 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.refactoring;

import com.intellij.codeInsight.unwrap.ScopeHighlighter;
import com.intellij.ide.IdeBundle;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.popup.JBPopupAdapter;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.LightweightWindowEvent;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.search.searches.OverridingMethodsSearch;
import com.intellij.psi.util.PsiFormatUtil;
import com.intellij.psi.util.PsiFormatUtilBase;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.ui.components.JBList;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.asJava.KtLightMethod;
import org.jetbrains.kotlin.asJava.LightClassUtilsKt;
import org.jetbrains.kotlin.builtins.KotlinBuiltIns;
import org.jetbrains.kotlin.descriptors.CallableDescriptor;
import org.jetbrains.kotlin.descriptors.ClassDescriptor;
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor;
import org.jetbrains.kotlin.descriptors.impl.LocalVariableDescriptor;
import org.jetbrains.kotlin.idea.KotlinBundle;
import org.jetbrains.kotlin.idea.caches.resolve.ResolutionUtils;
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils;
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde;
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers;
import org.jetbrains.kotlin.idea.util.string.StringUtilKt;
import org.jetbrains.kotlin.psi.*;
import org.jetbrains.kotlin.psi.psiUtil.JetPsiUtilKt;
import org.jetbrains.kotlin.renderer.DescriptorRenderer;
import org.jetbrains.kotlin.resolve.BindingContext;
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils;
import org.jetbrains.kotlin.resolve.DescriptorUtils;
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode;
import org.jetbrains.kotlin.types.KotlinType;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.util.*;
import java.util.List;

public class JetRefactoringUtil {

    private JetRefactoringUtil() {
    }

    @NotNull
    public static String wrapOrSkip(@NotNull String s, boolean inCode) {
        return inCode ? "<code>" + s + "</code>" : s;
    }

    @NotNull
    public static String formatClassDescriptor(@NotNull DeclarationDescriptor classDescriptor) {
        return IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_IN_TYPES.render(classDescriptor);
    }

    @NotNull
    public static String formatPsiClass(@NotNull PsiClass psiClass, boolean markAsJava, boolean inCode) {
        String description;

        String kind = psiClass.isInterface() ? "interface " : "class ";
        description = kind + PsiFormatUtil.formatClass(psiClass, PsiFormatUtilBase.SHOW_CONTAINING_CLASS
                | PsiFormatUtilBase.SHOW_NAME | PsiFormatUtilBase.SHOW_PARAMETERS | PsiFormatUtilBase.SHOW_TYPE);
        description = wrapOrSkip(description, inCode);

        return markAsJava ? "[Java] " + description : description;
    }

    @NotNull
    public static List<? extends PsiElement> checkSuperMethods(@NotNull KtDeclaration declaration,
            @Nullable Collection<PsiElement> ignore, @NotNull String actionStringKey) {
        BindingContext bindingContext = ResolutionUtils.analyze(declaration, BodyResolveMode.FULL);

        CallableDescriptor declarationDescriptor = (CallableDescriptor) bindingContext
                .get(BindingContext.DECLARATION_TO_DESCRIPTOR, declaration);

        if (declarationDescriptor == null || declarationDescriptor instanceof LocalVariableDescriptor) {
            return Collections.singletonList(declaration);
        }

        Project project = declaration.getProject();
        Map<PsiElement, CallableDescriptor> overriddenElementsToDescriptor = new HashMap<PsiElement, CallableDescriptor>();
        for (CallableDescriptor overriddenDescriptor : DescriptorUtils
                .getAllOverriddenDescriptors(declarationDescriptor)) {
            PsiElement overriddenDeclaration = DescriptorToSourceUtilsIde.INSTANCE$.getAnyDeclaration(project,
                    overriddenDescriptor);
            if (PsiTreeUtil.instanceOf(overriddenDeclaration, KtNamedFunction.class, KtProperty.class,
                    PsiMethod.class)) {
                overriddenElementsToDescriptor.put(overriddenDeclaration, overriddenDescriptor);
            }
        }
        if (ignore != null) {
            overriddenElementsToDescriptor.keySet().removeAll(ignore);
        }

        if (overriddenElementsToDescriptor.isEmpty())
            return Collections.singletonList(declaration);

        List<String> superClasses = getClassDescriptions(overriddenElementsToDescriptor);
        return askUserForMethodsToSearch(declaration, declarationDescriptor, overriddenElementsToDescriptor,
                superClasses, actionStringKey);
    }

    @NotNull
    private static List<? extends PsiElement> askUserForMethodsToSearch(@NotNull KtDeclaration declaration,
            @NotNull CallableDescriptor declarationDescriptor,
            @NotNull Map<PsiElement, CallableDescriptor> overriddenElementsToDescriptor,
            @NotNull List<String> superClasses, @NotNull String actionStringKey) {
        if (ApplicationManager.getApplication().isUnitTestMode()) {
            return ContainerUtil.newArrayList(overriddenElementsToDescriptor.keySet());
        }

        String superClassesStr = "\n" + StringUtil.join(superClasses, "");
        String message = KotlinBundle.message("x.overrides.y.in.class.list",
                DescriptorRenderer.COMPACT_WITH_SHORT_TYPES.render(declarationDescriptor),
                IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_IN_TYPES
                        .render(declarationDescriptor.getContainingDeclaration()),
                superClassesStr, KotlinBundle.message(actionStringKey));

        int exitCode = Messages.showYesNoCancelDialog(declaration.getProject(), message,
                IdeBundle.message("title.warning"), Messages.getQuestionIcon());
        switch (exitCode) {
        case Messages.YES:
            return ContainerUtil.newArrayList(overriddenElementsToDescriptor.keySet());
        case Messages.NO:
            return Collections.singletonList(declaration);
        default:
            return Collections.emptyList();
        }
    }

    @NotNull
    private static List<String> getClassDescriptions(
            @NotNull Map<PsiElement, CallableDescriptor> overriddenElementsToDescriptor) {
        return ContainerUtil.map(overriddenElementsToDescriptor.entrySet(),
                new Function<Map.Entry<PsiElement, CallableDescriptor>, String>() {
                    @Override
                    public String fun(Map.Entry<PsiElement, CallableDescriptor> entry) {
                        String description;

                        PsiElement element = entry.getKey();
                        CallableDescriptor descriptor = entry.getValue();
                        if (element instanceof KtNamedFunction || element instanceof KtProperty) {
                            description = formatClassDescriptor(descriptor.getContainingDeclaration());
                        } else {
                            assert element instanceof PsiMethod : "Invalid element: " + element.getText();

                            PsiClass psiClass = ((PsiMethod) element).getContainingClass();
                            assert psiClass != null : "Invalid element: " + element.getText();

                            description = formatPsiClass(psiClass, true, false);
                        }

                        return "    " + description + "\n";
                    }
                });
    }

    @NotNull
    public static String formatClass(@NotNull DeclarationDescriptor classDescriptor, boolean inCode) {
        PsiElement element = DescriptorToSourceUtils.descriptorToDeclaration(classDescriptor);
        if (element instanceof PsiClass) {
            return formatPsiClass((PsiClass) element, false, inCode);
        }

        return wrapOrSkip(formatClassDescriptor(classDescriptor), inCode);
    }

    @NotNull
    public static String formatFunction(@NotNull DeclarationDescriptor functionDescriptor, boolean inCode) {
        PsiElement element = DescriptorToSourceUtils.descriptorToDeclaration(functionDescriptor);
        if (element instanceof PsiMethod) {
            return formatPsiMethod((PsiMethod) element, false, inCode);
        }

        return wrapOrSkip(formatFunctionDescriptor(functionDescriptor), inCode);
    }

    @NotNull
    private static String formatFunctionDescriptor(@NotNull DeclarationDescriptor functionDescriptor) {
        return DescriptorRenderer.COMPACT.render(functionDescriptor);
    }

    @NotNull
    public static String formatPsiMethod(@NotNull PsiMethod psiMethod, boolean showContainingClass,
            boolean inCode) {
        int options = PsiFormatUtilBase.SHOW_NAME | PsiFormatUtilBase.SHOW_PARAMETERS | PsiFormatUtilBase.SHOW_TYPE;
        if (showContainingClass) {
            //noinspection ConstantConditions
            options |= PsiFormatUtilBase.SHOW_CONTAINING_CLASS;
        }

        String description = PsiFormatUtil.formatMethod(psiMethod, PsiSubstitutor.EMPTY, options,
                PsiFormatUtilBase.SHOW_TYPE);
        description = wrapOrSkip(description, inCode);

        return "[Java] " + description;
    }

    @NotNull
    public static String formatJavaOrLightMethod(@NotNull PsiMethod method) {
        PsiElement originalDeclaration = LightClassUtilsKt.getUnwrapped(method);
        if (originalDeclaration instanceof KtDeclaration) {
            KtDeclaration ktDeclaration = (KtDeclaration) originalDeclaration;
            BindingContext bindingContext = ResolutionUtils.analyze(ktDeclaration, BodyResolveMode.FULL);
            DeclarationDescriptor descriptor = bindingContext.get(BindingContext.DECLARATION_TO_DESCRIPTOR,
                    ktDeclaration);

            if (descriptor != null)
                return formatFunctionDescriptor(descriptor);
        }
        return formatPsiMethod(method, false, false);
    }

    @NotNull
    public static String formatClass(@NotNull KtClassOrObject classOrObject) {
        BindingContext bindingContext = ResolutionUtils.analyze(classOrObject, BodyResolveMode.FULL);
        DeclarationDescriptor descriptor = bindingContext.get(BindingContext.DECLARATION_TO_DESCRIPTOR,
                classOrObject);

        if (descriptor instanceof ClassDescriptor)
            return formatClassDescriptor(descriptor);
        return "class " + classOrObject.getName();
    }

    @Nullable
    public static Collection<? extends PsiElement> checkParametersInMethodHierarchy(
            @NotNull PsiParameter parameter) {
        PsiMethod method = (PsiMethod) parameter.getDeclarationScope();

        Set<PsiElement> parametersToDelete = collectParametersHierarchy(method, parameter);
        if (parametersToDelete.size() > 1) {
            if (ApplicationManager.getApplication().isUnitTestMode()) {
                return parametersToDelete;
            }

            String message = KotlinBundle.message("delete.param.in.method.hierarchy",
                    formatJavaOrLightMethod(method));
            int exitCode = Messages.showOkCancelDialog(parameter.getProject(), message,
                    IdeBundle.message("title.warning"), Messages.getQuestionIcon());
            if (exitCode == Messages.OK) {
                return parametersToDelete;
            } else {
                return null;
            }
        }

        return parametersToDelete;
    }

    // TODO: generalize breadth-first search
    @NotNull
    private static Set<PsiElement> collectParametersHierarchy(@NotNull PsiMethod method,
            @NotNull PsiParameter parameter) {
        Deque<PsiMethod> queue = new ArrayDeque<PsiMethod>();
        Set<PsiMethod> visited = new HashSet<PsiMethod>();
        Set<PsiElement> parametersToDelete = new HashSet<PsiElement>();

        queue.add(method);
        while (!queue.isEmpty()) {
            PsiMethod currentMethod = queue.poll();

            visited.add(currentMethod);
            addParameter(currentMethod, parametersToDelete, parameter);

            for (PsiMethod superMethod : currentMethod.findSuperMethods(true)) {
                if (!visited.contains(superMethod)) {
                    queue.offer(superMethod);
                }
            }
            for (PsiMethod overrider : OverridingMethodsSearch.search(currentMethod)) {
                if (!visited.contains(overrider)) {
                    queue.offer(overrider);
                }
            }
        }
        return parametersToDelete;
    }

    private static void addParameter(@NotNull PsiMethod method, @NotNull Set<PsiElement> result,
            @NotNull PsiParameter parameter) {
        int parameterIndex = JetPsiUtilKt.parameterIndex(LightClassUtilsKt.getUnwrapped(parameter));

        if (method instanceof KtLightMethod) {
            KtDeclaration declaration = ((KtLightMethod) method).getOrigin();
            if (declaration instanceof KtFunction) {
                result.add(((KtFunction) declaration).getValueParameters().get(parameterIndex));
            }
        } else {
            result.add(method.getParameterList().getParameters()[parameterIndex]);
        }
    }

    public interface SelectExpressionCallback {
        void run(@Nullable KtExpression expression);
    }

    public static void selectExpression(@NotNull Editor editor, @NotNull PsiFile file,
            @NotNull SelectExpressionCallback callback) throws IntroduceRefactoringException {
        selectExpression(editor, file, true, callback);
    }

    public static void selectExpression(@NotNull Editor editor, @NotNull PsiFile file,
            boolean failOnEmptySuggestion, @NotNull SelectExpressionCallback callback)
            throws IntroduceRefactoringException {
        if (editor.getSelectionModel().hasSelection()) {
            int selectionStart = editor.getSelectionModel().getSelectionStart();
            int selectionEnd = editor.getSelectionModel().getSelectionEnd();
            String text = file.getText();
            while (selectionStart < selectionEnd && Character.isSpaceChar(text.charAt(selectionStart)))
                ++selectionStart;
            while (selectionStart < selectionEnd && Character.isSpaceChar(text.charAt(selectionEnd - 1)))
                --selectionEnd;
            callback.run(findExpression(file, selectionStart, selectionEnd, failOnEmptySuggestion));
        } else {
            int offset = editor.getCaretModel().getOffset();
            smartSelectExpression(editor, file, offset, failOnEmptySuggestion, callback);
        }
    }

    public static List<KtExpression> getSmartSelectSuggestions(@NotNull PsiFile file, int offset)
            throws IntroduceRefactoringException {
        if (offset < 0) {
            return new ArrayList<KtExpression>();
        }

        PsiElement element = file.findElementAt(offset);
        if (element == null) {
            return new ArrayList<KtExpression>();
        }
        if (element instanceof PsiWhiteSpace) {
            return getSmartSelectSuggestions(file, offset - 1);
        }

        List<KtExpression> expressions = new ArrayList<KtExpression>();
        while (element != null
                && !(element instanceof KtBlockExpression && !(element.getParent() instanceof KtFunctionLiteral))
                && !(element instanceof KtNamedFunction) && !(element instanceof KtClassBody)) {
            if (element instanceof KtExpression && !(element instanceof KtStatementExpression)) {
                boolean addExpression = true;

                if (KtPsiUtil.isLabelIdentifierExpression(element)) {
                    addExpression = false;
                } else if (element.getParent() instanceof KtQualifiedExpression) {
                    KtQualifiedExpression qualifiedExpression = (KtQualifiedExpression) element.getParent();
                    if (qualifiedExpression.getReceiverExpression() != element) {
                        addExpression = false;
                    }
                } else if (element.getParent() instanceof KtCallElement
                        || element.getParent() instanceof KtThisExpression
                        || PsiTreeUtil.getParentOfType(element, KtSuperExpression.class) != null) {
                    addExpression = false;
                } else if (element.getParent() instanceof KtOperationExpression) {
                    KtOperationExpression operationExpression = (KtOperationExpression) element.getParent();
                    if (operationExpression.getOperationReference() == element) {
                        addExpression = false;
                    }
                }
                if (addExpression) {
                    KtExpression expression = (KtExpression) element;
                    BindingContext bindingContext = ResolutionUtils.analyze(expression, BodyResolveMode.FULL);
                    KotlinType expressionType = bindingContext.getType(expression);
                    if (expressionType == null || !KotlinBuiltIns.isUnit(expressionType)) {
                        expressions.add(expression);
                    }
                }
            } else if (element instanceof KtTypeElement) {
                expressions.clear();
            }
            element = element.getParent();
        }
        return expressions;
    }

    private static void smartSelectExpression(@NotNull Editor editor, @NotNull PsiFile file, int offset,
            boolean failOnEmptySuggestion, @NotNull final SelectExpressionCallback callback)
            throws IntroduceRefactoringException {
        List<KtExpression> expressions = getSmartSelectSuggestions(file, offset);
        if (expressions.size() == 0) {
            if (failOnEmptySuggestion)
                throw new IntroduceRefactoringException(
                        JetRefactoringBundle.message("cannot.refactor.not.expression"));
            return;
        }

        if (expressions.size() == 1 || ApplicationManager.getApplication().isUnitTestMode()) {
            callback.run(expressions.get(0));
            return;
        }

        final DefaultListModel model = new DefaultListModel();
        for (KtExpression expression : expressions) {
            model.addElement(expression);
        }

        final ScopeHighlighter highlighter = new ScopeHighlighter(editor);

        final JList list = new JBList(model);

        list.setCellRenderer(new DefaultListCellRenderer() {
            @Override
            public Component getListCellRendererComponent(@NotNull JList list, Object value, int index,
                    boolean isSelected, boolean cellHasFocus) {
                Component rendererComponent = super.getListCellRendererComponent(list, value, index, isSelected,
                        cellHasFocus);
                KtExpression element = (KtExpression) value;
                if (element.isValid()) {
                    setText(getExpressionShortText(element));
                }
                return rendererComponent;
            }
        });

        list.addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(@NotNull ListSelectionEvent e) {
                highlighter.dropHighlight();
                int selectedIndex = list.getSelectedIndex();
                if (selectedIndex < 0)
                    return;
                KtExpression expression = (KtExpression) model.get(selectedIndex);
                List<PsiElement> toExtract = new ArrayList<PsiElement>();
                toExtract.add(expression);
                highlighter.highlight(expression, toExtract);
            }
        });

        JBPopupFactory.getInstance().createListPopupBuilder(list)
                .setTitle(JetRefactoringBundle.message("expressions.title")).setMovable(false).setResizable(false)
                .setRequestFocus(true).setItemChoosenCallback(new Runnable() {
                    @Override
                    public void run() {
                        callback.run((KtExpression) list.getSelectedValue());
                    }
                }).addListener(new JBPopupAdapter() {
                    @Override
                    public void onClosed(LightweightWindowEvent event) {
                        highlighter.dropHighlight();
                    }
                }).createPopup().showInBestPositionFor(editor);

    }

    public static String getExpressionShortText(@NotNull KtElement element) { //todo: write appropriate implementation
        return StringUtilKt.collapseSpaces(StringUtil.shortenTextWithEllipsis(element.getText(), 53, 0));
    }

    @Nullable
    private static KtExpression findExpression(@NotNull PsiFile file, int startOffset, int endOffset,
            boolean failOnNoExpression) throws IntroduceRefactoringException {
        KtExpression element = CodeInsightUtils.findExpression(file, startOffset, endOffset);
        if (element == null) {
            //todo: if it's infix expression => add (), then commit document then return new created expression

            if (failOnNoExpression) {
                throw new IntroduceRefactoringException(
                        JetRefactoringBundle.message("cannot.refactor.not.expression"));
            }
            return null;
        }
        return element;
    }

    public static class IntroduceRefactoringException extends Exception {
        private final String myMessage;

        public IntroduceRefactoringException(String message) {
            myMessage = message;
        }

        @Override
        public String getMessage() {
            return myMessage;
        }
    }

}