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