move fileScope and filesScope back from GlobalSearchScopes to GlobalSearchScope
[idea/community.git] / plugins / properties / src / com / intellij / codeInspection / duplicatePropertyInspection / DuplicatePropertyInspection.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.intellij.codeInspection.duplicatePropertyInspection;
17
18 import com.intellij.codeInspection.*;
19 import com.intellij.codeInspection.ex.GlobalInspectionContextImpl;
20 import com.intellij.codeInspection.reference.RefManager;
21 import com.intellij.concurrency.JobUtil;
22 import com.intellij.lang.properties.IProperty;
23 import com.intellij.lang.properties.PropertiesBundle;
24 import com.intellij.lang.properties.psi.PropertiesFile;
25 import com.intellij.lang.properties.psi.Property;
26 import com.intellij.openapi.diagnostic.Logger;
27 import com.intellij.openapi.editor.Document;
28 import com.intellij.openapi.fileEditor.FileDocumentManager;
29 import com.intellij.openapi.module.Module;
30 import com.intellij.openapi.module.ModuleUtil;
31 import com.intellij.openapi.progress.ProcessCanceledException;
32 import com.intellij.openapi.progress.ProgressIndicator;
33 import com.intellij.openapi.progress.ProgressManager;
34 import com.intellij.openapi.progress.util.ProgressWrapper;
35 import com.intellij.openapi.util.Comparing;
36 import com.intellij.openapi.util.text.StringUtil;
37 import com.intellij.openapi.vfs.VirtualFile;
38 import com.intellij.psi.PsiElement;
39 import com.intellij.psi.PsiFile;
40 import com.intellij.psi.impl.search.LowLevelSearchUtil;
41 import com.intellij.psi.search.GlobalSearchScope;
42 import com.intellij.psi.search.PsiSearchHelper;
43 import com.intellij.util.CommonProcessors;
44 import com.intellij.util.Processor;
45 import com.intellij.util.text.CharArrayUtil;
46 import com.intellij.util.text.StringSearcher;
47 import gnu.trove.THashSet;
48 import org.jetbrains.annotations.NotNull;
49
50 import javax.swing.*;
51 import java.awt.event.ActionEvent;
52 import java.awt.event.ActionListener;
53 import java.net.MalformedURLException;
54 import java.net.URL;
55 import java.util.*;
56
57 public class DuplicatePropertyInspection extends GlobalSimpleInspectionTool {
58   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInspection.DuplicatePropertyInspection");
59
60   public boolean CURRENT_FILE = true;
61   public boolean MODULE_WITH_DEPENDENCIES = false;
62
63   public boolean CHECK_DUPLICATE_VALUES = true;
64   public boolean CHECK_DUPLICATE_KEYS = true;
65   public boolean CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES = true;
66
67   @Override
68   public void checkFile(@NotNull PsiFile file,
69                         @NotNull InspectionManager manager,
70                         @NotNull ProblemsHolder problemsHolder,
71                         @NotNull GlobalInspectionContext globalContext,
72                         @NotNull ProblemDescriptionsProcessor problemDescriptionsProcessor) {
73     checkFile(file, manager, (GlobalInspectionContextImpl)globalContext, globalContext.getRefManager(), problemDescriptionsProcessor);
74   }
75
76   //public HTMLComposerImpl getComposer() {
77   //  return new DescriptorComposer(this) {
78   //    protected void composeDescription(final CommonProblemDescriptor description, int i, StringBuffer buf, final RefEntity refElement) {
79   //      @NonNls String descriptionTemplate = description.getDescriptionTemplate();
80   //      descriptionTemplate = descriptionTemplate.replaceAll("#end", " ");
81   //      buf.append(descriptionTemplate);
82   //    }
83   //  };
84   //}
85
86   @SuppressWarnings({"HardCodedStringLiteral"})
87   private static void surroundWithHref(StringBuffer anchor, PsiElement element, final boolean isValue) {
88     if (element != null) {
89       final PsiElement parent = element.getParent();
90       PsiElement elementToLink = isValue ? parent.getFirstChild() : parent.getLastChild();
91       if (elementToLink != null) {
92         HTMLComposer.appendAfterHeaderIndention(anchor);
93         HTMLComposer.appendAfterHeaderIndention(anchor);
94         anchor.append("<a HREF=\"");
95         try {
96           final PsiFile file = element.getContainingFile();
97           if (file != null) {
98             final VirtualFile virtualFile = file.getVirtualFile();
99             if (virtualFile != null) {
100               anchor.append(new URL(virtualFile.getUrl() + "#" + elementToLink.getTextRange().getStartOffset()));
101             }
102           }
103         }
104         catch (MalformedURLException e) {
105           LOG.error(e);
106         }
107         anchor.append("\">");
108         anchor.append(elementToLink.getText().replaceAll("\\$", "\\\\\\$"));
109         anchor.append("</a>");
110         compoundLineLink(anchor, element);
111         anchor.append("<br>");
112       }
113     }
114     else {
115       anchor.append("<font style=\"font-family:verdana; font-weight:bold; color:#FF0000\";>");
116       anchor.append(InspectionsBundle.message("inspection.export.results.invalidated.item"));
117       anchor.append("</font>");
118     }
119   }
120
121   @SuppressWarnings({"HardCodedStringLiteral"})
122   private static void compoundLineLink(StringBuffer lineAnchor, PsiElement psiElement) {
123     final PsiFile file = psiElement.getContainingFile();
124     if (file != null) {
125       final VirtualFile vFile = file.getVirtualFile();
126       if (vFile != null) {
127         Document doc = FileDocumentManager.getInstance().getDocument(vFile);
128         final int lineNumber = doc.getLineNumber(psiElement.getTextOffset()) + 1;
129         lineAnchor.append(" ").append(InspectionsBundle.message("inspection.export.results.at.line")).append(" ");
130         lineAnchor.append("<a HREF=\"");
131         try {
132           int offset = doc.getLineStartOffset(lineNumber - 1);
133           offset = CharArrayUtil.shiftForward(doc.getCharsSequence(), offset, " \t");
134           lineAnchor.append(new URL(vFile.getUrl() + "#" + offset));
135         }
136         catch (MalformedURLException e) {
137           LOG.error(e);
138         }
139         lineAnchor.append("\">");
140         lineAnchor.append(Integer.toString(lineNumber));
141         lineAnchor.append("</a>");
142       }
143     }
144   }
145
146   private void checkFile(final PsiFile file, final InspectionManager manager, GlobalInspectionContextImpl context, final RefManager refManager, final ProblemDescriptionsProcessor processor) {
147     if (!(file instanceof PropertiesFile)) return;
148     if (!context.isToCheckMember(file, this)) return;
149     final PsiSearchHelper searchHelper = PsiSearchHelper.SERVICE.getInstance(file.getProject());
150     final PropertiesFile propertiesFile = (PropertiesFile)file;
151     final List<IProperty> properties = propertiesFile.getProperties();
152     Module module = ModuleUtil.findModuleForPsiElement(file);
153     if (module == null) return;
154     final GlobalSearchScope scope = CURRENT_FILE
155                                     ? GlobalSearchScope.fileScope(file)
156                                     : MODULE_WITH_DEPENDENCIES
157                                       ? GlobalSearchScope.moduleWithDependenciesScope(module)
158                                       : GlobalSearchScope.projectScope(file.getProject());
159     final Map<String, Set<PsiFile>> processedValueToFiles = Collections.synchronizedMap(new HashMap<String, Set<PsiFile>>());
160     final Map<String, Set<PsiFile>> processedKeyToFiles = Collections.synchronizedMap(new HashMap<String, Set<PsiFile>>());
161     final ProgressIndicator original = ProgressManager.getInstance().getProgressIndicator();
162     final ProgressIndicator progress = ProgressWrapper.wrap(original);
163     ProgressManager.getInstance().runProcess(new Runnable() {
164       public void run() {
165         if (!JobUtil.invokeConcurrentlyUnderProgress(properties, progress, false, new Processor<IProperty>() {
166           public boolean process(final IProperty property) {
167             if (original != null) {
168               if (original.isCanceled()) return false;
169               original.setText2(PropertiesBundle.message("searching.for.property.key.progress.text", property.getUnescapedKey()));
170             }
171             processTextUsages(processedValueToFiles, property.getValue(), processedKeyToFiles, searchHelper, scope);
172             processTextUsages(processedKeyToFiles, property.getUnescapedKey(), processedValueToFiles, searchHelper, scope);
173             return true;
174           }
175         })) throw new ProcessCanceledException();
176
177         List<ProblemDescriptor> problemDescriptors = new ArrayList<ProblemDescriptor>();
178         Map<String, Set<String>> keyToDifferentValues = new HashMap<String, Set<String>>();
179         if (CHECK_DUPLICATE_KEYS || CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES) {
180           prepareDuplicateKeysByFile(processedKeyToFiles, manager, keyToDifferentValues, problemDescriptors, file, original);
181         }
182         if (CHECK_DUPLICATE_VALUES) prepareDuplicateValuesByFile(processedValueToFiles, manager, problemDescriptors, file, original);
183         if (CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES) {
184           processDuplicateKeysWithDifferentValues(keyToDifferentValues, processedKeyToFiles, problemDescriptors, manager, file, original);
185         }
186         if (!problemDescriptors.isEmpty()) {
187           processor.addProblemElement(refManager.getReference(file),
188                                       problemDescriptors.toArray(new ProblemDescriptor[problemDescriptors.size()]));
189         }
190       }
191     }, progress);
192   }
193
194   private static void processTextUsages(final Map<String, Set<PsiFile>> processedTextToFiles,
195                                         final String text,
196                                         final Map<String, Set<PsiFile>> processedFoundTextToFiles,
197                                         final PsiSearchHelper searchHelper,
198                                         final GlobalSearchScope scope) {
199     if (!processedTextToFiles.containsKey(text)) {
200       if (processedFoundTextToFiles.containsKey(text)) {
201         final Set<PsiFile> filesWithValue = processedFoundTextToFiles.get(text);
202         processedTextToFiles.put(text, filesWithValue);
203       }
204       else {
205         final Set<PsiFile> resultFiles = new HashSet<PsiFile>();
206         findFilesWithText(text, searchHelper, scope, resultFiles);
207         if (resultFiles.isEmpty()) return;
208         processedTextToFiles.put(text, resultFiles);
209       }
210     }
211   }
212
213
214   private static void prepareDuplicateValuesByFile(final Map<String, Set<PsiFile>> valueToFiles,
215                                                    final InspectionManager manager,
216                                                    final List<ProblemDescriptor> problemDescriptors,
217                                                    final PsiFile psiFile,
218                                                    final ProgressIndicator progress) {
219     for (String value : valueToFiles.keySet()) {
220       if (progress != null){
221         progress.setText2(InspectionsBundle.message("duplicate.property.value.progress.indicator.text", value));
222         progress.checkCanceled();
223       }
224       if (value.length() == 0) continue;
225       StringSearcher searcher = new StringSearcher(value, true, true);
226       StringBuffer message = new StringBuffer();
227       int duplicatesCount = 0;
228       Set<PsiFile> psiFilesWithDuplicates = valueToFiles.get(value);
229       for (PsiFile file : psiFilesWithDuplicates) {
230         CharSequence text = file.getViewProvider().getContents();
231         for (int offset = LowLevelSearchUtil.searchWord(text, 0, text.length(), searcher, progress);
232              offset >= 0;
233              offset = LowLevelSearchUtil.searchWord(text, offset + searcher.getPattern().length(), text.length(), searcher, progress)
234           ) {
235           PsiElement element = file.findElementAt(offset);
236           if (element != null && element.getParent() instanceof Property) {
237             final Property property = (Property)element.getParent();
238             if (Comparing.equal(property.getValue(), value) && element.getStartOffsetInParent() != 0) {
239               if (duplicatesCount == 0){
240                 message.append(InspectionsBundle.message("duplicate.property.value.problem.descriptor", property.getValue()));
241               }
242               surroundWithHref(message, element, true);
243               duplicatesCount ++;
244             }
245           }
246         }
247       }
248       if (duplicatesCount > 1) {
249         problemDescriptors.add(manager.createProblemDescriptor(psiFile, message.toString(), false, null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING));
250       }
251     }
252
253
254   }
255
256   private void prepareDuplicateKeysByFile(final Map<String, Set<PsiFile>> keyToFiles,
257                                           final InspectionManager manager,
258                                           final Map<String, Set<String>> keyToValues,
259                                           final List<ProblemDescriptor> problemDescriptors,
260                                           final PsiFile psiFile,
261                                           final ProgressIndicator progress) {
262     for (String key : keyToFiles.keySet()) {
263       if (progress!= null){
264         progress.setText2(InspectionsBundle.message("duplicate.property.key.progress.indicator.text", key));
265         if (progress.isCanceled()) throw new ProcessCanceledException();
266       }
267       final StringBuffer message = new StringBuffer();
268       int duplicatesCount = 0;
269       Set<PsiFile> psiFilesWithDuplicates = keyToFiles.get(key);
270       for (PsiFile file : psiFilesWithDuplicates) {
271         if (!(file instanceof PropertiesFile)) continue;
272         PropertiesFile propertiesFile = (PropertiesFile)file;
273         final List<IProperty> propertiesByKey = propertiesFile.findPropertiesByKey(key);
274         for (IProperty property : propertiesByKey) {
275           if (duplicatesCount == 0){
276             message.append(InspectionsBundle.message("duplicate.property.key.problem.descriptor", key));
277           }
278           surroundWithHref(message, property.getPsiElement().getFirstChild(), false);
279           duplicatesCount ++;
280           //prepare for filter same keys different values
281           Set<String> values = keyToValues.get(key);
282           if (values == null){
283             values = new HashSet<String>();
284             keyToValues.put(key, values);
285           }
286           values.add(property.getValue());
287         }
288       }
289       if (duplicatesCount > 1 && CHECK_DUPLICATE_KEYS) {
290         problemDescriptors.add(manager.createProblemDescriptor(psiFile, message.toString(), false, null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING));
291       }
292     }
293
294   }
295
296
297   private static void processDuplicateKeysWithDifferentValues(final Map<String, Set<String>> keyToDifferentValues,
298                                                               final Map<String, Set<PsiFile>> keyToFiles,
299                                                               final List<ProblemDescriptor> problemDescriptors,
300                                                               final InspectionManager manager,
301                                                               final PsiFile psiFile,
302                                                               final ProgressIndicator progress) {
303     for (String key : keyToDifferentValues.keySet()) {
304       if (progress != null) {
305         progress.setText2(InspectionsBundle.message("duplicate.property.diff.key.progress.indicator.text", key));
306         if (progress.isCanceled()) throw new ProcessCanceledException();
307       }
308       final Set<String> values = keyToDifferentValues.get(key);
309       if (values == null || values.size() < 2){
310         keyToFiles.remove(key);
311       } else {
312         StringBuffer message = new StringBuffer();
313         final Set<PsiFile> psiFiles = keyToFiles.get(key);
314         boolean firstUsage = true;
315         for (PsiFile file : psiFiles) {
316           if (!(file instanceof PropertiesFile)) continue;
317           PropertiesFile propertiesFile = (PropertiesFile)file;
318           final List<IProperty> propertiesByKey = propertiesFile.findPropertiesByKey(key);
319           for (IProperty property : propertiesByKey) {
320             if (firstUsage){
321               message.append(InspectionsBundle.message("duplicate.property.diff.key.problem.descriptor", key));
322               firstUsage = false;
323             }
324             surroundWithHref(message, property.getPsiElement().getFirstChild(), false);
325           }
326         }
327         problemDescriptors.add(manager.createProblemDescriptor(psiFile, message.toString(), false, null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING));
328       }
329     }
330   }
331
332   private static void findFilesWithText(String stringToFind,
333                                         PsiSearchHelper searchHelper,
334                                         GlobalSearchScope scope,
335                                         final Set<PsiFile> resultFiles) {
336     final List<String> words = StringUtil.getWordsIn(stringToFind);
337     if (words.isEmpty()) return;
338     Collections.sort(words, new Comparator<String>() {
339       public int compare(final String o1, final String o2) {
340         return o2.length() - o1.length();
341       }
342     });
343     for (String word : words) {
344       final Set<PsiFile> files = new THashSet<PsiFile>();
345       searchHelper.processAllFilesWithWord(word, scope, new CommonProcessors.CollectProcessor<PsiFile>(files), true);
346       if (resultFiles.isEmpty()) {
347         resultFiles.addAll(files);
348       }
349       else {
350         resultFiles.retainAll(files);
351       }
352       if (resultFiles.isEmpty()) return;
353     }
354   }
355
356   @NotNull
357   public String getDisplayName() {
358     return InspectionsBundle.message("duplicate.property.display.name");
359   }
360
361   @NotNull
362   public String getGroupDisplayName() {
363     return InspectionsBundle.message("group.names.internationalization.issues");
364   }
365
366   @NotNull
367   public String getShortName() {
368     return "DuplicatePropertyInspection";
369   }
370
371   public boolean isEnabledByDefault() {
372     return false;
373   }
374
375   public JComponent createOptionsPanel() {
376     return new OptionsPanel().myWholePanel;
377   }
378
379   public class OptionsPanel {
380     private JRadioButton myFileScope;
381     private JRadioButton myModuleScope;
382     private JRadioButton myProjectScope;
383     private JCheckBox myDuplicateValues;
384     private JCheckBox myDuplicateKeys;
385     private JCheckBox myDuplicateBoth;
386     private JPanel myWholePanel;
387
388     OptionsPanel() {
389       ButtonGroup buttonGroup = new ButtonGroup();
390       buttonGroup.add(myFileScope);
391       buttonGroup.add(myModuleScope);
392       buttonGroup.add(myProjectScope);
393
394       myFileScope.setSelected(CURRENT_FILE);
395       myModuleScope.setSelected(MODULE_WITH_DEPENDENCIES);
396       myProjectScope.setSelected(!(CURRENT_FILE || MODULE_WITH_DEPENDENCIES));
397
398       myFileScope.addActionListener(new ActionListener() {
399         public void actionPerformed(ActionEvent e) {
400           CURRENT_FILE = myFileScope.isSelected();
401         }
402       });
403       myModuleScope.addActionListener(new ActionListener() {
404         public void actionPerformed(ActionEvent e) {
405           MODULE_WITH_DEPENDENCIES = myModuleScope.isSelected();
406           if (MODULE_WITH_DEPENDENCIES) {
407             CURRENT_FILE = false;
408           }
409         }
410       });
411       myProjectScope.addActionListener(new ActionListener() {
412         public void actionPerformed(ActionEvent e) {
413           if (myProjectScope.isSelected()) {
414             CURRENT_FILE = false;
415             MODULE_WITH_DEPENDENCIES = false;
416           }
417         }
418       });
419
420       myDuplicateKeys.setSelected(CHECK_DUPLICATE_KEYS);
421       myDuplicateValues.setSelected(CHECK_DUPLICATE_VALUES);
422       myDuplicateBoth.setSelected(CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES);
423
424       myDuplicateKeys.addActionListener(new ActionListener() {
425         public void actionPerformed(ActionEvent e) {
426           CHECK_DUPLICATE_KEYS = myDuplicateKeys.isSelected();
427         }
428       });
429       myDuplicateValues.addActionListener(new ActionListener() {
430         public void actionPerformed(ActionEvent e) {
431           CHECK_DUPLICATE_VALUES = myDuplicateValues.isSelected();
432         }
433       });
434       myDuplicateBoth.addActionListener(new ActionListener() {
435         public void actionPerformed(ActionEvent e) {
436           CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES = myDuplicateBoth.isSelected();
437         }
438       });
439     }
440   }
441 }