2 * Copyright 2000-2015 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.refactoring.typeMigration.inspections;
18 import com.intellij.codeInsight.FileModificationService;
19 import com.intellij.codeInspection.*;
20 import com.intellij.codeInspection.ui.MultipleCheckboxOptionsPanel;
21 import com.intellij.openapi.command.undo.UndoUtil;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.editor.Editor;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.util.AtomicNotNullLazyValue;
26 import com.intellij.psi.*;
27 import com.intellij.psi.search.GlobalSearchScope;
28 import com.intellij.psi.search.GlobalSearchScopesCore;
29 import com.intellij.psi.util.PsiTreeUtil;
30 import com.intellij.psi.util.PsiTypesUtil;
31 import com.intellij.psi.util.PsiUtil;
32 import com.intellij.refactoring.typeMigration.TypeMigrationProcessor;
33 import com.intellij.refactoring.typeMigration.TypeMigrationRules;
34 import com.intellij.refactoring.typeMigration.rules.TypeConversionRule;
35 import com.intellij.refactoring.typeMigration.rules.guava.*;
36 import com.intellij.reference.SoftLazyValue;
37 import com.intellij.util.Function;
38 import com.intellij.util.IncorrectOperationException;
39 import com.intellij.util.containers.ContainerUtil;
40 import com.intellij.util.containers.hash.HashMap;
41 import org.jetbrains.annotations.Nls;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
49 * @author Dmitry Batkovich
51 @SuppressWarnings("DialogTitleCapitalization")
52 public class GuavaInspection extends BaseJavaLocalInspectionTool {
53 //public class GuavaInspection extends BaseJavaBatchLocalInspectionTool {
54 private final static Logger LOG = Logger.getInstance(GuavaInspection.class);
56 public final static String PROBLEM_DESCRIPTION = "Guava's functional primitives can be replaced by Java API";
58 private final static SoftLazyValue<Set<String>> FLUENT_ITERABLE_STOP_METHODS = new SoftLazyValue<Set<String>>() {
61 protected Set<String> compute() {
62 return ContainerUtil.newHashSet("append", "cycle", "uniqueIndex", "index");
66 public boolean checkVariables = true;
67 public boolean checkChains = true;
68 public boolean checkReturnTypes = true;
69 public boolean ignoreJavaxNullable = true;
71 @SuppressWarnings("Duplicates")
73 public JComponent createOptionsPanel() {
74 final MultipleCheckboxOptionsPanel panel = new MultipleCheckboxOptionsPanel(this);
75 panel.addCheckbox("Report variables", "checkVariables");
76 panel.addCheckbox("Report method chains", "checkChains");
77 panel.addCheckbox("Report return types", "checkReturnTypes");
78 panel.addCheckbox("Erase @javax.annotations.Nullable from converted functions", "ignoreJavaxNullable");
84 public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
85 if (!PsiUtil.isLanguageLevel8OrHigher(holder.getFile())) {
86 return PsiElementVisitor.EMPTY_VISITOR;
88 return new JavaElementVisitor() {
89 private final AtomicNotNullLazyValue<Map<String, PsiClass>> myGuavaClassConversions =
90 new AtomicNotNullLazyValue<Map<String, PsiClass>>() {
93 protected Map<String, PsiClass> compute() {
94 Map<String, PsiClass> map = new HashMap<String, PsiClass>();
95 for (TypeConversionRule rule : TypeConversionRule.EP_NAME.getExtensions()) {
96 if (rule instanceof BaseGuavaTypeConversionRule) {
97 final String fromClass = ((BaseGuavaTypeConversionRule)rule).ruleFromClass();
98 final String toClass = ((BaseGuavaTypeConversionRule)rule).ruleToClass();
100 final Project project = holder.getProject();
101 final JavaPsiFacade javaPsiFacade = JavaPsiFacade.getInstance(project);
102 final PsiClass targetClass = javaPsiFacade.findClass(toClass, GlobalSearchScope.allScope(project));
104 if (targetClass != null) {
105 map.put(fromClass, targetClass);
114 public void visitVariable(PsiVariable variable) {
115 if (!checkVariables) return;
116 final PsiType type = variable.getType();
117 PsiType targetType = getConversionClassType(type);
118 if (targetType != null) {
119 holder.registerProblem(variable.getNameIdentifier(),
121 new MigrateGuavaTypeFix(variable, targetType));
126 public void visitMethod(PsiMethod method) {
127 super.visitMethod(method);
128 if (!checkReturnTypes) return;
129 final PsiType targetType = getConversionClassType(method.getReturnType());
130 if (targetType != null) {
131 final PsiTypeElement typeElement = method.getReturnTypeElement();
132 if (typeElement != null) {
133 holder.registerProblem(typeElement,
135 new MigrateGuavaTypeFix(method, targetType));
141 public void visitMethodCallExpression(PsiMethodCallExpression expression) {
142 checkFluentIterableGenerationMethod(expression);
143 checkPredicatesUtilityMethod(expression);
146 private void checkPredicatesUtilityMethod(PsiMethodCallExpression expression) {
147 if (GuavaPredicateConversionRule.isPredicates(expression)) {
148 final PsiClassType initialType = (PsiClassType)expression.getType();
149 PsiClassType targetType = createTargetType(initialType);
150 if (targetType == null) return;
151 holder.registerProblem(expression.getMethodExpression().getReferenceNameElement(),
153 new MigrateGuavaTypeFix(expression, targetType));
157 private void checkFluentIterableGenerationMethod(PsiMethodCallExpression expression) {
158 if (!checkChains) return;
159 if (!isFluentIterableFromCall(expression)) return;
161 final PsiMethodCallExpression chain = findGuavaMethodChain(expression);
166 PsiClassType initialType = (PsiClassType)chain.getType();
167 LOG.assertTrue(initialType != null);
168 PsiClassType targetType = createTargetType(initialType);
169 if (targetType == null) return;
171 PsiElement highlightedElement = chain;
172 if (chain.getParent() instanceof PsiReferenceExpression && chain.getParent().getParent() instanceof PsiMethodCallExpression) {
173 highlightedElement = chain.getParent().getParent();
175 holder.registerProblem(highlightedElement, PROBLEM_DESCRIPTION, new MigrateGuavaTypeFix(chain, targetType));
179 private PsiClassType createTargetType(PsiClassType initialType) {
180 PsiClass resolvedClass = initialType.resolve();
182 if (resolvedClass == null || (target = myGuavaClassConversions.getValue().get(resolvedClass.getQualifiedName())) == null) {
185 return addTypeParameters(initialType, initialType.resolveGenerics(), target);
188 private PsiType getConversionClassType(PsiType initialType) {
189 if (initialType == null) return null;
190 final PsiType type = initialType.getDeepComponentType();
191 if (type instanceof PsiClassType) {
192 final PsiClassType.ClassResolveResult resolveResult = ((PsiClassType)type).resolveGenerics();
193 final PsiClass psiClass = resolveResult.getElement();
194 if (psiClass != null) {
195 final String qName = psiClass.getQualifiedName();
196 final PsiClass targetClass = myGuavaClassConversions.getValue().get(qName);
197 if (targetClass != null) {
198 final PsiClassType createdType = addTypeParameters(type, resolveResult, targetClass);
199 return initialType instanceof PsiArrayType ? wrapAsArray((PsiArrayType)initialType, createdType) : createdType;
208 private PsiType wrapAsArray(PsiArrayType initial, PsiType created) {
209 PsiArrayType result = new PsiArrayType(created);
210 while (initial.getComponentType() instanceof PsiArrayType) {
211 initial = (PsiArrayType)initial.getComponentType();
212 result = new PsiArrayType(result);
217 private boolean isFluentIterableFromCall(PsiMethodCallExpression expression) {
218 PsiMethod method = expression.resolveMethod();
219 if (method == null || !GuavaFluentIterableConversionRule.CHAIN_HEAD_METHODS.contains(method.getName())) {
222 PsiClass aClass = method.getContainingClass();
223 return aClass != null && (GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(aClass.getQualifiedName()) ||
224 GuavaFluentIterableConversionRule.FLUENT_ITERABLE.equals(aClass.getQualifiedName()));
227 private PsiMethodCallExpression findGuavaMethodChain(PsiMethodCallExpression expression) {
228 PsiMethodCallExpression chain = expression;
230 final PsiMethodCallExpression current = PsiTreeUtil.getParentOfType(chain, PsiMethodCallExpression.class);
231 if (current != null && current.getMethodExpression().getQualifierExpression() == chain) {
232 final PsiMethod method = current.resolveMethod();
233 if (method == null) {
236 if (FLUENT_ITERABLE_STOP_METHODS.getValue().contains(method.getName())) {
239 final PsiClass containingClass = method.getContainingClass();
240 if (containingClass == null || !(GuavaFluentIterableConversionRule.FLUENT_ITERABLE.equals(containingClass.getQualifiedName())
241 || GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(containingClass.getQualifiedName()))) {
244 final PsiType returnType = method.getReturnType();
245 final PsiClass returnClass = PsiTypesUtil.getPsiClass(returnType);
246 if (returnClass == null || !(GuavaFluentIterableConversionRule.FLUENT_ITERABLE.equals(returnClass.getQualifiedName())
247 || GuavaOptionalConversionRule.GUAVA_OPTIONAL.equals(returnClass.getQualifiedName()))) {
250 if (GuavaTypeConversionDescriptor.isIterable(current)) {
262 private PsiClassType addTypeParameters(PsiType currentType,
263 PsiClassType.ClassResolveResult currentTypeResolveResult,
264 PsiClass targetClass) {
265 final Map<PsiTypeParameter, PsiType> substitutionMap = currentTypeResolveResult.getSubstitutor().getSubstitutionMap();
266 final PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(holder.getProject());
267 if (substitutionMap.size() == 1) {
268 return elementFactory.createType(targetClass, ContainerUtil.getFirstItem(substitutionMap.values()));
271 LOG.assertTrue(substitutionMap.size() == 2);
272 LOG.assertTrue(GuavaLambda.FUNCTION.getJavaAnalogueClassQName().equals(targetClass.getQualifiedName()));
273 final PsiType returnType = LambdaUtil.getFunctionalInterfaceReturnType(currentType);
274 final List<PsiType> types = new ArrayList<PsiType>(substitutionMap.values());
275 types.remove(returnType);
276 final PsiType parameterType = types.get(0);
277 return elementFactory.createType(targetClass, parameterType, returnType);
283 public class MigrateGuavaTypeFix extends LocalQuickFixAndIntentionActionOnPsiElement implements BatchQuickFix<ProblemDescriptor> {
284 private final PsiType myTargetType;
286 private MigrateGuavaTypeFix(@NotNull PsiElement element, PsiType targetType) {
288 myTargetType = targetType;
292 public void invoke(@NotNull Project project,
293 @NotNull PsiFile file,
294 @Nullable("is null when called from inspection") Editor editor,
295 @NotNull PsiElement startElement,
296 @NotNull PsiElement endElement) {
297 performTypeMigration(Collections.singletonList(startElement), Collections.singletonList(myTargetType));
301 protected boolean isAvailable() {
302 return super.isAvailable() && myTargetType.isValid();
307 public String getText() {
308 final PsiElement element = getStartElement();
309 if (!myTargetType.isValid() || !element.isValid()) {
310 return getFamilyName();
312 final String presentation;
313 if (element instanceof PsiMethodCallExpression) {
314 presentation = "method chain";
317 presentation = TypeMigrationProcessor.getPresentation(element);
319 return "Migrate " + presentation + " type to '" + myTargetType.getCanonicalText(false) + "'";
325 public String getFamilyName() {
326 return "Migrate Guava's type to Java";
330 public boolean startInWriteAction() {
335 public void applyFix(@NotNull final Project project,
336 @NotNull ProblemDescriptor[] descriptors,
337 @NotNull List<PsiElement> psiElementsToIgnore,
338 @Nullable Runnable refreshViews) {
339 final List<PsiElement> elementsToFix = new ArrayList<PsiElement>();
340 final List<PsiType> migrationTypes = new ArrayList<PsiType>();
342 for (ProblemDescriptor descriptor : descriptors) {
343 final MigrateGuavaTypeFix fix = getFix(descriptor);
344 elementsToFix.add(fix.getStartElement());
345 migrationTypes.add(fix.myTargetType);
348 if (!elementsToFix.isEmpty()) performTypeMigration(elementsToFix, migrationTypes);
351 private MigrateGuavaTypeFix getFix(ProblemDescriptor descriptor) {
352 final QuickFix[] fixes = descriptor.getFixes();
353 LOG.assertTrue(fixes != null);
354 for (QuickFix fix : fixes) {
355 if (fix instanceof MigrateGuavaTypeFix) {
356 return (MigrateGuavaTypeFix)fix;
359 throw new AssertionError();
362 private boolean performTypeMigration(List<PsiElement> elements, List<PsiType> types) {
363 PsiFile containingFile = null;
364 for (PsiElement element : elements) {
365 final PsiFile currentContainingFile = element.getContainingFile();
366 if (containingFile == null) {
367 containingFile = currentContainingFile;
370 LOG.assertTrue(containingFile.isEquivalentTo(currentContainingFile));
373 LOG.assertTrue(containingFile != null);
374 if (!FileModificationService.getInstance().prepareFileForWrite(containingFile)) return false;
376 final TypeMigrationRules rules = new TypeMigrationRules();
377 rules.setBoundScope(GlobalSearchScopesCore.projectProductionScope(containingFile.getProject())
378 .union(GlobalSearchScopesCore.projectTestScope(containingFile.getProject())));
379 rules.addConversionRuleSettings(new GuavaConversionSettings(ignoreJavaxNullable));
380 TypeMigrationProcessor.runHighlightingTypeMigration(containingFile.getProject(),
383 elements.toArray(new PsiElement[elements.size()]),
384 createMigrationTypeFunction(elements, types),
386 UndoUtil.markPsiFileForUndo(containingFile);
388 catch (IncorrectOperationException e) {
394 private Function<PsiElement, PsiType> createMigrationTypeFunction(@NotNull final List<PsiElement> elements,
395 @NotNull final List<PsiType> types) {
396 LOG.assertTrue(elements.size() == types.size());
397 final Map<PsiElement, PsiType> mappings = new HashMap<PsiElement, PsiType>();
398 final Iterator<PsiType> typeIterator = types.iterator();
399 for (PsiElement element : elements) {
400 PsiType type = typeIterator.next();
401 mappings.put(element, type);
403 return element -> mappings.get(element);