Inspections - pass onTheFly into ProblemDescriptors & use it to create LAZY refs...
[fedora-idea.git] / plugins / properties / src / com / intellij / codeInspection / duplicatePropertyInspection / DuplicatePropertyInspection.java
blob2e4cdadf83573bcb6c9d1cd1fc8e7021246425a3
1 /*
2 * Copyright 2000-2009 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.codeInspection.duplicatePropertyInspection;
18 import com.intellij.analysis.AnalysisScope;
19 import com.intellij.codeInspection.*;
20 import com.intellij.codeInspection.ex.DescriptorComposer;
21 import com.intellij.codeInspection.ex.DescriptorProviderInspection;
22 import com.intellij.codeInspection.ex.HTMLComposerImpl;
23 import com.intellij.codeInspection.ex.JobDescriptor;
24 import com.intellij.codeInspection.reference.RefEntity;
25 import com.intellij.concurrency.JobUtil;
26 import com.intellij.lang.properties.PropertiesBundle;
27 import com.intellij.lang.properties.psi.PropertiesFile;
28 import com.intellij.lang.properties.psi.Property;
29 import com.intellij.openapi.diagnostic.Logger;
30 import com.intellij.openapi.editor.Document;
31 import com.intellij.openapi.fileEditor.FileDocumentManager;
32 import com.intellij.openapi.module.Module;
33 import com.intellij.openapi.module.ModuleUtil;
34 import com.intellij.openapi.progress.ProcessCanceledException;
35 import com.intellij.openapi.progress.ProgressIndicator;
36 import com.intellij.openapi.progress.ProgressManager;
37 import com.intellij.openapi.progress.util.ProgressWrapper;
38 import com.intellij.openapi.util.Comparing;
39 import com.intellij.openapi.util.text.StringUtil;
40 import com.intellij.openapi.vfs.VirtualFile;
41 import com.intellij.psi.PsiElement;
42 import com.intellij.psi.PsiFile;
43 import com.intellij.psi.PsiRecursiveElementVisitor;
44 import com.intellij.psi.impl.search.LowLevelSearchUtil;
45 import com.intellij.psi.search.GlobalSearchScope;
46 import com.intellij.psi.search.PsiSearchHelper;
47 import com.intellij.util.CommonProcessors;
48 import com.intellij.util.Processor;
49 import com.intellij.util.text.CharArrayUtil;
50 import com.intellij.util.text.StringSearcher;
51 import gnu.trove.THashSet;
52 import org.jetbrains.annotations.NonNls;
53 import org.jetbrains.annotations.NotNull;
55 import javax.swing.*;
56 import java.awt.event.ActionEvent;
57 import java.awt.event.ActionListener;
58 import java.net.MalformedURLException;
59 import java.net.URL;
60 import java.util.*;
62 public class DuplicatePropertyInspection extends DescriptorProviderInspection {
63 private static final Logger LOG = Logger.getInstance("#com.intellij.codeInspection.DuplicatePropertyInspection");
65 public boolean CURRENT_FILE = true;
66 public boolean MODULE_WITH_DEPENDENCIES = false;
68 public boolean CHECK_DUPLICATE_VALUES = true;
69 public boolean CHECK_DUPLICATE_KEYS = true;
70 public boolean CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES = true;
72 private JRadioButton myFileScope;
73 private JRadioButton myModuleScope;
74 private JRadioButton myProjectScope;
75 private JCheckBox myDuplicateValues;
76 private JCheckBox myDuplicateKeys;
77 private JCheckBox myDuplicateBoth;
78 private JPanel myWholePanel;
80 public void runInspection(AnalysisScope scope, final InspectionManager manager) {
81 scope.accept(new PsiRecursiveElementVisitor() {
82 @Override public void visitFile(PsiFile file) {
83 checkFile(file, manager);
85 });
88 public HTMLComposerImpl getComposer() {
89 return new DescriptorComposer(this) {
90 protected void composeDescription(final CommonProblemDescriptor description, int i, StringBuffer buf, final RefEntity refElement) {
91 @NonNls String descriptionTemplate = description.getDescriptionTemplate();
92 descriptionTemplate = descriptionTemplate.replaceAll("#end", " ");
93 buf.append(descriptionTemplate);
98 @SuppressWarnings({"HardCodedStringLiteral"})
99 private static void surroundWithHref(StringBuffer anchor, PsiElement element, final boolean isValue) {
100 if (element != null) {
101 final PsiElement parent = element.getParent();
102 PsiElement elementToLink = isValue ? parent.getFirstChild() : parent.getLastChild();
103 if (elementToLink != null) {
104 HTMLComposer.appendAfterHeaderIndention(anchor);
105 HTMLComposer.appendAfterHeaderIndention(anchor);
106 anchor.append("<a HREF=\"");
107 try {
108 final PsiFile file = element.getContainingFile();
109 if (file != null) {
110 final VirtualFile virtualFile = file.getVirtualFile();
111 if (virtualFile != null) {
112 anchor.append(new URL(virtualFile.getUrl() + "#" + elementToLink.getTextRange().getStartOffset()));
116 catch (MalformedURLException e) {
117 LOG.error(e);
119 anchor.append("\">");
120 anchor.append(elementToLink.getText().replaceAll("\\$", "\\\\\\$"));
121 anchor.append("</a>");
122 compoundLineLink(anchor, element);
123 anchor.append("<br>");
126 else {
127 anchor.append("<font style=\"font-family:verdana; font-weight:bold; color:#FF0000\";>");
128 anchor.append(InspectionsBundle.message("inspection.export.results.invalidated.item"));
129 anchor.append("</font>");
133 @SuppressWarnings({"HardCodedStringLiteral"})
134 private static void compoundLineLink(StringBuffer lineAnchor, PsiElement psiElement) {
135 final PsiFile file = psiElement.getContainingFile();
136 if (file != null) {
137 final VirtualFile vFile = file.getVirtualFile();
138 if (vFile != null) {
139 Document doc = FileDocumentManager.getInstance().getDocument(vFile);
140 final int lineNumber = doc.getLineNumber(psiElement.getTextOffset()) + 1;
141 lineAnchor.append(" ").append(InspectionsBundle.message("inspection.export.results.at.line")).append(" ");
142 lineAnchor.append("<a HREF=\"");
143 try {
144 int offset = doc.getLineStartOffset(lineNumber - 1);
145 offset = CharArrayUtil.shiftForward(doc.getCharsSequence(), offset, " \t");
146 lineAnchor.append(new URL(vFile.getUrl() + "#" + offset));
148 catch (MalformedURLException e) {
149 LOG.error(e);
151 lineAnchor.append("\">");
152 lineAnchor.append(Integer.toString(lineNumber));
153 lineAnchor.append("</a>");
158 @NotNull
159 public JobDescriptor[] getJobDescriptors() {
160 return JobDescriptor.EMPTY_ARRAY;
163 private void checkFile(final PsiFile file, final InspectionManager manager) {
164 if (!(file instanceof PropertiesFile)) return;
165 if (!getContext().isToCheckMember(file, this)) return;
166 final PsiSearchHelper searchHelper = file.getManager().getSearchHelper();
167 final PropertiesFile propertiesFile = (PropertiesFile)file;
168 final List<Property> properties = propertiesFile.getProperties();
169 Module module = ModuleUtil.findModuleForPsiElement(file);
170 if (module == null) return;
171 final GlobalSearchScope scope = CURRENT_FILE
172 ? GlobalSearchScope.fileScope(file)
173 : MODULE_WITH_DEPENDENCIES
174 ? GlobalSearchScope.moduleWithDependenciesScope(module)
175 : GlobalSearchScope.projectScope(file.getProject());
176 final Map<String, Set<PsiFile>> processedValueToFiles = Collections.synchronizedMap(new HashMap<String, Set<PsiFile>>());
177 final Map<String, Set<PsiFile>> processedKeyToFiles = Collections.synchronizedMap(new HashMap<String, Set<PsiFile>>());
178 final ProgressIndicator original = ProgressManager.getInstance().getProgressIndicator();
179 final ProgressIndicator progress = ProgressWrapper.wrap(original);
180 ProgressManager.getInstance().runProcess(new Runnable() {
181 public void run() {
182 JobUtil.invokeConcurrentlyUnderMyProgress(properties, new Processor<Property>() {
183 public boolean process(final Property property) {
184 if (original != null) {
185 if (original.isCanceled()) return false;
186 original.setText2(PropertiesBundle.message("searching.for.property.key.progress.text", property.getUnescapedKey()));
188 processTextUsages(processedValueToFiles, property.getValue(), processedKeyToFiles, searchHelper, scope);
189 processTextUsages(processedKeyToFiles, property.getUnescapedKey(), processedValueToFiles, searchHelper, scope);
190 return true;
192 }, "Searching properties usages");
194 List<ProblemDescriptor> problemDescriptors = new ArrayList<ProblemDescriptor>();
195 Map<String, Set<String>> keyToDifferentValues = new HashMap<String, Set<String>>();
196 if (CHECK_DUPLICATE_KEYS || CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES) {
197 prepareDuplicateKeysByFile(processedKeyToFiles, manager, keyToDifferentValues, problemDescriptors, file, original);
199 if (CHECK_DUPLICATE_VALUES) prepareDuplicateValuesByFile(processedValueToFiles, manager, problemDescriptors, file, original);
200 if (CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES) {
201 processDuplicateKeysWithDifferentValues(keyToDifferentValues, processedKeyToFiles, problemDescriptors, manager, file, original);
203 if (!problemDescriptors.isEmpty()) {
204 addProblemElement(getRefManager().getReference(file),
205 problemDescriptors.toArray(new ProblemDescriptor[problemDescriptors.size()]));
208 }, progress);
211 private static void processTextUsages(final Map<String, Set<PsiFile>> processedTextToFiles,
212 final String text,
213 final Map<String, Set<PsiFile>> processedFoundTextToFiles,
214 final PsiSearchHelper searchHelper,
215 final GlobalSearchScope scope) {
216 if (!processedTextToFiles.containsKey(text)) {
217 if (processedFoundTextToFiles.containsKey(text)) {
218 final Set<PsiFile> filesWithValue = processedFoundTextToFiles.get(text);
219 processedTextToFiles.put(text, filesWithValue);
221 else {
222 final Set<PsiFile> resultFiles = new HashSet<PsiFile>();
223 findFilesWithText(text, searchHelper, scope, resultFiles);
224 if (resultFiles.isEmpty()) return;
225 processedTextToFiles.put(text, resultFiles);
231 private static void prepareDuplicateValuesByFile(final Map<String, Set<PsiFile>> valueToFiles,
232 final InspectionManager manager,
233 final List<ProblemDescriptor> problemDescriptors,
234 final PsiFile psiFile,
235 final ProgressIndicator progress) {
236 for (String value : valueToFiles.keySet()) {
237 if (progress != null){
238 progress.setText2(InspectionsBundle.message("duplicate.property.value.progress.indicator.text", value));
239 if (progress.isCanceled()) throw new ProcessCanceledException();
241 StringSearcher searcher = new StringSearcher(value, true, true);
242 StringBuffer message = new StringBuffer();
243 int duplicatesCount = 0;
244 Set<PsiFile> psiFilesWithDuplicates = valueToFiles.get(value);
245 for (PsiFile file : psiFilesWithDuplicates) {
246 CharSequence text = file.getViewProvider().getContents();
247 for (int offset = LowLevelSearchUtil.searchWord(text, 0, text.length(), searcher);
248 offset >= 0;
249 offset = LowLevelSearchUtil.searchWord(text, offset + searcher.getPattern().length(), text.length(), searcher)
251 PsiElement element = file.findElementAt(offset);
252 if (element != null && element.getParent() instanceof Property) {
253 final Property property = (Property)element.getParent();
254 if (Comparing.equal(property.getValue(), value) && element.getStartOffsetInParent() != 0) {
255 if (duplicatesCount == 0){
256 message.append(InspectionsBundle.message("duplicate.property.value.problem.descriptor", property.getValue()));
258 surroundWithHref(message, element, true);
259 duplicatesCount ++;
264 if (duplicatesCount > 1) {
265 problemDescriptors.add(manager.createProblemDescriptor(psiFile, message.toString(),
266 (LocalQuickFix[])null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
267 false));
274 private void prepareDuplicateKeysByFile(final Map<String, Set<PsiFile>> keyToFiles,
275 final InspectionManager manager,
276 final Map<String, Set<String>> keyToValues,
277 final List<ProblemDescriptor> problemDescriptors,
278 final PsiFile psiFile,
279 final ProgressIndicator progress) {
280 for (String key : keyToFiles.keySet()) {
281 if (progress!= null){
282 progress.setText2(InspectionsBundle.message("duplicate.property.key.progress.indicator.text", key));
283 if (progress.isCanceled()) throw new ProcessCanceledException();
285 final StringBuffer message = new StringBuffer();
286 int duplicatesCount = 0;
287 Set<PsiFile> psiFilesWithDuplicates = keyToFiles.get(key);
288 for (PsiFile file : psiFilesWithDuplicates) {
289 if (!(file instanceof PropertiesFile)) continue;
290 PropertiesFile propertiesFile = (PropertiesFile)file;
291 final List<Property> propertiesByKey = propertiesFile.findPropertiesByKey(key);
292 for (Property property : propertiesByKey) {
293 if (duplicatesCount == 0){
294 message.append(InspectionsBundle.message("duplicate.property.key.problem.descriptor", key));
296 surroundWithHref(message, property.getFirstChild(), false);
297 duplicatesCount ++;
298 //prepare for filter same keys different values
299 Set<String> values = keyToValues.get(key);
300 if (values == null){
301 values = new HashSet<String>();
302 keyToValues.put(key, values);
304 values.add(property.getValue());
307 if (duplicatesCount > 1 && CHECK_DUPLICATE_KEYS) {
308 problemDescriptors.add(manager.createProblemDescriptor(psiFile, message.toString(),
309 (LocalQuickFix[])null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
310 false));
317 private static void processDuplicateKeysWithDifferentValues(final Map<String, Set<String>> keyToDifferentValues,
318 final Map<String, Set<PsiFile>> keyToFiles,
319 final List<ProblemDescriptor> problemDescriptors,
320 final InspectionManager manager,
321 final PsiFile psiFile,
322 final ProgressIndicator progress) {
323 for (String key : keyToDifferentValues.keySet()) {
324 if (progress != null) {
325 progress.setText2(InspectionsBundle.message("duplicate.property.diff.key.progress.indicator.text", key));
326 if (progress.isCanceled()) throw new ProcessCanceledException();
328 final Set<String> values = keyToDifferentValues.get(key);
329 if (values == null || values.size() < 2){
330 keyToFiles.remove(key);
331 } else {
332 StringBuffer message = new StringBuffer();
333 final Set<PsiFile> psiFiles = keyToFiles.get(key);
334 boolean firstUsage = true;
335 for (PsiFile file : psiFiles) {
336 if (!(file instanceof PropertiesFile)) continue;
337 PropertiesFile propertiesFile = (PropertiesFile)file;
338 final List<Property> propertiesByKey = propertiesFile.findPropertiesByKey(key);
339 for (Property property : propertiesByKey) {
340 if (firstUsage){
341 message.append(InspectionsBundle.message("duplicate.property.diff.key.problem.descriptor", key));
342 firstUsage = false;
344 surroundWithHref(message, property.getFirstChild(), false);
347 problemDescriptors.add(manager.createProblemDescriptor(psiFile, message.toString(),
348 (LocalQuickFix[])null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
349 false));
354 private static void findFilesWithText(String stringToFind,
355 PsiSearchHelper searchHelper,
356 GlobalSearchScope scope,
357 final Set<PsiFile> resultFiles) {
358 final List<String> words = StringUtil.getWordsIn(stringToFind);
359 if (words.isEmpty()) return;
360 Collections.sort(words, new Comparator<String>() {
361 public int compare(final String o1, final String o2) {
362 return o2.length() - o1.length();
365 for (String word : words) {
366 final Set<PsiFile> files = new THashSet<PsiFile>();
367 searchHelper.processAllFilesWithWord(word, scope, new CommonProcessors.CollectProcessor<PsiFile>(files), true);
368 if (resultFiles.isEmpty()) {
369 resultFiles.addAll(files);
371 else {
372 resultFiles.retainAll(files);
374 if (resultFiles.isEmpty()) return;
378 @NotNull
379 public String getDisplayName() {
380 return InspectionsBundle.message("duplicate.property.display.name");
383 @NotNull
384 public String getGroupDisplayName() {
385 return InspectionsBundle.message("group.names.internationalization.issues");
388 @NotNull
389 public String getShortName() {
390 return "DuplicatePropertyInspection";
393 public boolean isEnabledByDefault() {
394 return false;
397 public JComponent createOptionsPanel() {
398 ButtonGroup buttonGroup = new ButtonGroup();
399 buttonGroup.add(myFileScope);
400 buttonGroup.add(myModuleScope);
401 buttonGroup.add(myProjectScope);
403 myFileScope.setSelected(CURRENT_FILE);
404 myModuleScope.setSelected(MODULE_WITH_DEPENDENCIES);
405 myProjectScope.setSelected(!(CURRENT_FILE || MODULE_WITH_DEPENDENCIES));
407 myFileScope.addActionListener(new ActionListener() {
408 public void actionPerformed(ActionEvent e) {
409 CURRENT_FILE = myFileScope.isSelected();
412 myModuleScope.addActionListener(new ActionListener() {
413 public void actionPerformed(ActionEvent e) {
414 MODULE_WITH_DEPENDENCIES = myModuleScope.isSelected();
415 if (MODULE_WITH_DEPENDENCIES){
416 CURRENT_FILE = false;
420 myProjectScope.addActionListener(new ActionListener() {
421 public void actionPerformed(ActionEvent e) {
422 if (myProjectScope.isSelected()){
423 CURRENT_FILE = false;
424 MODULE_WITH_DEPENDENCIES = false;
429 myDuplicateKeys.setSelected(CHECK_DUPLICATE_KEYS);
430 myDuplicateValues.setSelected(CHECK_DUPLICATE_VALUES);
431 myDuplicateBoth.setSelected(CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES);
433 myDuplicateKeys.addActionListener(new ActionListener() {
434 public void actionPerformed(ActionEvent e) {
435 CHECK_DUPLICATE_KEYS = myDuplicateKeys.isSelected();
438 myDuplicateValues.addActionListener(new ActionListener() {
439 public void actionPerformed(ActionEvent e) {
440 CHECK_DUPLICATE_VALUES = myDuplicateValues.isSelected();
443 myDuplicateBoth.addActionListener(new ActionListener() {
444 public void actionPerformed(ActionEvent e) {
445 CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES = myDuplicateBoth.isSelected();
448 return myWholePanel;