/* * Copyright 2013-2016 Sergey Ignatov, Alexander Zolotov, Florin Patan * * 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.goide.codeInsight.imports; import com.goide.GoTypes; import com.goide.psi.*; import com.goide.psi.impl.GoReferenceBase; import com.intellij.lang.ImportOptimizer; import com.intellij.openapi.editor.Document; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.EmptyRunnable; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.MultiMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; public class GoImportOptimizer implements ImportOptimizer { @Override public boolean supports(PsiFile file) { return file instanceof GoFile; } @NotNull @Override public Runnable processFile(@NotNull PsiFile file) { if (!(file instanceof GoFile)) { return EmptyRunnable.getInstance(); } MultiMap<String, GoImportSpec> importMap = ((GoFile)file).getImportMap(); Set<PsiElement> importEntriesToDelete = ContainerUtil.newLinkedHashSet(); Set<PsiElement> importIdentifiersToDelete = findRedundantImportIdentifiers(importMap); importEntriesToDelete.addAll(findDuplicatedEntries(importMap)); importEntriesToDelete.addAll(filterUnusedImports(file, importMap).values()); if (importEntriesToDelete.isEmpty() && importIdentifiersToDelete.isEmpty()) { return EmptyRunnable.getInstance(); } return new CollectingInfoRunnable() { @Nullable @Override public String getUserNotificationInfo() { int entriesToDelete = importEntriesToDelete.size(); int identifiersToDelete = importIdentifiersToDelete.size(); String result = ""; if (entriesToDelete > 0) { result = "Removed " + entriesToDelete + " import" + (entriesToDelete > 1 ? "s" : ""); } if (identifiersToDelete > 0) { result += result.isEmpty() ? "Removed " : " and "; result += identifiersToDelete + " alias" + (identifiersToDelete > 1 ? "es" : ""); } return StringUtil.nullize(result); } @Override public void run() { if (!importEntriesToDelete.isEmpty() || !importIdentifiersToDelete.isEmpty()) { PsiDocumentManager manager = PsiDocumentManager.getInstance(file.getProject()); Document document = manager.getDocument(file); if (document != null) { manager.commitDocument(document); } } for (PsiElement importEntry : importEntriesToDelete) { if (importEntry != null && importEntry.isValid()) { deleteImportSpec(getImportSpec(importEntry)); } } for (PsiElement identifier : importIdentifiersToDelete) { if (identifier != null && identifier.isValid()) { identifier.delete(); } } } }; } @NotNull public static Set<PsiElement> findRedundantImportIdentifiers(@NotNull MultiMap<String, GoImportSpec> importMap) { Set<PsiElement> importIdentifiersToDelete = ContainerUtil.newLinkedHashSet(); for (PsiElement importEntry : importMap.values()) { GoImportSpec importSpec = getImportSpec(importEntry); if (importSpec != null) { String localPackageName = importSpec.getLocalPackageName(); if (!StringUtil.isEmpty(localPackageName)) { if (Comparing.equal(importSpec.getAlias(), localPackageName)) { importIdentifiersToDelete.add(importSpec.getIdentifier()); } } } } return importIdentifiersToDelete; } public static MultiMap<String, GoImportSpec> filterUnusedImports(@NotNull PsiFile file, @NotNull MultiMap<String, GoImportSpec> importMap) { MultiMap<String, GoImportSpec> result = MultiMap.create(); result.putAllValues(importMap); result.remove("_"); // imports for side effects are always used Collection<GoImportSpec> implicitImports = ContainerUtil.newArrayList(result.get(".")); for (GoImportSpec importEntry : implicitImports) { GoImportSpec spec = getImportSpec(importEntry); if (spec != null && spec.isDot() && hasImportUsers(spec)) { result.remove(".", importEntry); } } file.accept(new GoRecursiveVisitor() { @Override public void visitTypeReferenceExpression(@NotNull GoTypeReferenceExpression o) { GoTypeReferenceExpression lastQualifier = o.getQualifier(); if (lastQualifier != null) { GoTypeReferenceExpression previousQualifier; while ((previousQualifier = lastQualifier.getQualifier()) != null) { lastQualifier = previousQualifier; } markAsUsed(lastQualifier.getIdentifier(), lastQualifier.getReference()); } } @Override public void visitReferenceExpression(@NotNull GoReferenceExpression o) { GoReferenceExpression lastQualifier = o.getQualifier(); if (lastQualifier != null) { GoReferenceExpression previousQualifier; while ((previousQualifier = lastQualifier.getQualifier()) != null) { lastQualifier = previousQualifier; } markAsUsed(lastQualifier.getIdentifier(), lastQualifier.getReference()); } } private void markAsUsed(@NotNull PsiElement qualifier, @NotNull PsiReference reference) { String qualifierText = qualifier.getText(); if (!result.containsKey(qualifierText)) { // already marked return; } PsiElement resolve = reference.resolve(); if (!(resolve instanceof PsiDirectory || resolve instanceof GoImportSpec || resolve instanceof PsiDirectoryContainer)) { return; } Collection<String> qualifiersToDelete = ContainerUtil.newHashSet(); for (GoImportSpec spec : result.get(qualifierText)) { for (Map.Entry<String, Collection<GoImportSpec>> entry : result.entrySet()) { for (GoImportSpec importSpec : entry.getValue()) { if (importSpec == spec) { qualifiersToDelete.add(entry.getKey()); } } } } for (String qualifierToDelete : qualifiersToDelete) { result.remove(qualifierToDelete); } } }); return result; } private static boolean hasImportUsers(@NotNull GoImportSpec spec) { //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (spec) { List<PsiElement> list = spec.getUserData(GoReferenceBase.IMPORT_USERS); if (list != null) { for (PsiElement e : list) { if (e.isValid()) { return true; } ProgressManager.checkCanceled(); } } } return false; } @NotNull public static Set<GoImportSpec> findDuplicatedEntries(@NotNull MultiMap<String, GoImportSpec> importMap) { Set<GoImportSpec> duplicatedEntries = ContainerUtil.newLinkedHashSet(); for (Map.Entry<String, Collection<GoImportSpec>> imports : importMap.entrySet()) { Collection<GoImportSpec> importsWithSameName = imports.getValue(); if (importsWithSameName.size() > 1) { MultiMap<String, GoImportSpec> importsWithSameString = collectImportsWithSameString(importsWithSameName); for (Map.Entry<String, Collection<GoImportSpec>> importWithSameString : importsWithSameString.entrySet()) { List<GoImportSpec> duplicates = ContainerUtil.newArrayList(importWithSameString.getValue()); if (duplicates.size() > 1) { duplicatedEntries.addAll(duplicates.subList(1, duplicates.size())); } } } } return duplicatedEntries; } private static void deleteImportSpec(@Nullable GoImportSpec importSpec) { GoImportDeclaration importDeclaration = PsiTreeUtil.getParentOfType(importSpec, GoImportDeclaration.class); if (importSpec != null && importDeclaration != null) { PsiElement startElementToDelete = importSpec; PsiElement endElementToDelete = importSpec; if (importDeclaration.getImportSpecList().size() == 1) { startElementToDelete = importDeclaration; endElementToDelete = importDeclaration; PsiElement nextSibling = endElementToDelete.getNextSibling(); if (nextSibling != null && nextSibling.getNode().getElementType() == GoTypes.SEMICOLON) { endElementToDelete = nextSibling; } } // todo: delete after proper formatter implementation PsiElement nextSibling = endElementToDelete.getNextSibling(); if (nextSibling instanceof PsiWhiteSpace && nextSibling.textContains('\n')) { endElementToDelete = nextSibling; } startElementToDelete.getParent().deleteChildRange(startElementToDelete, endElementToDelete); } } @NotNull private static MultiMap<String, GoImportSpec> collectImportsWithSameString(@NotNull Collection<GoImportSpec> importsWithSameName) { MultiMap<String, GoImportSpec> importsWithSameString = MultiMap.create(); for (PsiElement duplicateCandidate : importsWithSameName) { GoImportSpec importSpec = getImportSpec(duplicateCandidate); if (importSpec != null) { importsWithSameString.putValue(importSpec.getPath(), importSpec); } } return importsWithSameString; } @Nullable public static GoImportSpec getImportSpec(@NotNull PsiElement importEntry) { return PsiTreeUtil.getNonStrictParentOfType(importEntry, GoImportSpec.class); } }