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
;
56 import java
.awt
.event
.ActionEvent
;
57 import java
.awt
.event
.ActionListener
;
58 import java
.net
.MalformedURLException
;
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
);
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=\"");
108 final PsiFile file
= element
.getContainingFile();
110 final VirtualFile virtualFile
= file
.getVirtualFile();
111 if (virtualFile
!= null) {
112 anchor
.append(new URL(virtualFile
.getUrl() + "#" + elementToLink
.getTextRange().getStartOffset()));
116 catch (MalformedURLException e
) {
119 anchor
.append("\">");
120 anchor
.append(elementToLink
.getText().replaceAll("\\$", "\\\\\\$"));
121 anchor
.append("</a>");
122 compoundLineLink(anchor
, element
);
123 anchor
.append("<br>");
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();
137 final VirtualFile vFile
= file
.getVirtualFile();
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=\"");
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
) {
151 lineAnchor
.append("\">");
152 lineAnchor
.append(Integer
.toString(lineNumber
));
153 lineAnchor
.append("</a>");
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() {
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
);
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()]));
211 private static void processTextUsages(final Map
<String
, Set
<PsiFile
>> processedTextToFiles
,
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
);
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
);
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);
264 if (duplicatesCount
> 1) {
265 problemDescriptors
.add(manager
.createProblemDescriptor(psiFile
, message
.toString(),
266 (LocalQuickFix
[])null, ProblemHighlightType
.GENERIC_ERROR_OR_WARNING
,
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);
298 //prepare for filter same keys different values
299 Set
<String
> values
= keyToValues
.get(key
);
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
,
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
);
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
) {
341 message
.append(InspectionsBundle
.message("duplicate.property.diff.key.problem.descriptor", key
));
344 surroundWithHref(message
, property
.getFirstChild(), false);
347 problemDescriptors
.add(manager
.createProblemDescriptor(psiFile
, message
.toString(),
348 (LocalQuickFix
[])null, ProblemHighlightType
.GENERIC_ERROR_OR_WARNING
,
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
);
372 resultFiles
.retainAll(files
);
374 if (resultFiles
.isEmpty()) return;
379 public String
getDisplayName() {
380 return InspectionsBundle
.message("duplicate.property.display.name");
384 public String
getGroupDisplayName() {
385 return InspectionsBundle
.message("group.names.internationalization.issues");
389 public String
getShortName() {
390 return "DuplicatePropertyInspection";
393 public boolean isEnabledByDefault() {
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();