| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.codeInspection.duplicatePropertyInspection; |
| |
| import com.intellij.codeInspection.*; |
| import com.intellij.codeInspection.ex.GlobalInspectionContextBase; |
| import com.intellij.codeInspection.reference.RefManager; |
| import com.intellij.concurrency.JobLauncher; |
| import com.intellij.lang.properties.IProperty; |
| import com.intellij.lang.properties.PropertiesBundle; |
| import com.intellij.lang.properties.psi.PropertiesFile; |
| import com.intellij.lang.properties.psi.Property; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.fileEditor.FileDocumentManager; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.module.ModuleUtilCore; |
| import com.intellij.openapi.progress.ProcessCanceledException; |
| import com.intellij.openapi.progress.ProgressIndicator; |
| import com.intellij.openapi.progress.ProgressManager; |
| import com.intellij.openapi.progress.util.ProgressWrapper; |
| import com.intellij.openapi.util.Comparing; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.impl.search.LowLevelSearchUtil; |
| import com.intellij.psi.search.GlobalSearchScope; |
| import com.intellij.psi.search.PsiSearchHelper; |
| import com.intellij.util.CommonProcessors; |
| import com.intellij.util.Processor; |
| import com.intellij.util.text.CharArrayUtil; |
| import com.intellij.util.text.StringSearcher; |
| import gnu.trove.THashSet; |
| import org.jetbrains.annotations.NotNull; |
| |
| import javax.swing.*; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.ActionListener; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.*; |
| |
| public class DuplicatePropertyInspection extends GlobalSimpleInspectionTool { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.codeInspection.DuplicatePropertyInspection"); |
| |
| public boolean CURRENT_FILE = true; |
| public boolean MODULE_WITH_DEPENDENCIES = false; |
| |
| public boolean CHECK_DUPLICATE_VALUES = true; |
| public boolean CHECK_DUPLICATE_KEYS = true; |
| public boolean CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES = true; |
| |
| @Override |
| public void checkFile(@NotNull PsiFile file, |
| @NotNull InspectionManager manager, |
| @NotNull ProblemsHolder problemsHolder, |
| @NotNull GlobalInspectionContext globalContext, |
| @NotNull ProblemDescriptionsProcessor problemDescriptionsProcessor) { |
| checkFile(file, manager, (GlobalInspectionContextBase)globalContext, globalContext.getRefManager(), problemDescriptionsProcessor); |
| } |
| |
| @SuppressWarnings({"HardCodedStringLiteral"}) |
| private static void surroundWithHref(StringBuffer anchor, PsiElement element, final boolean isValue) { |
| if (element != null) { |
| final PsiElement parent = element.getParent(); |
| PsiElement elementToLink = isValue ? parent.getFirstChild() : parent.getLastChild(); |
| if (elementToLink != null) { |
| HTMLComposer.appendAfterHeaderIndention(anchor); |
| HTMLComposer.appendAfterHeaderIndention(anchor); |
| anchor.append("<a HREF=\""); |
| try { |
| final PsiFile file = element.getContainingFile(); |
| if (file != null) { |
| final VirtualFile virtualFile = file.getVirtualFile(); |
| if (virtualFile != null) { |
| anchor.append(new URL(virtualFile.getUrl() + "#" + elementToLink.getTextRange().getStartOffset())); |
| } |
| } |
| } |
| catch (MalformedURLException e) { |
| LOG.error(e); |
| } |
| anchor.append("\">"); |
| anchor.append(elementToLink.getText().replaceAll("\\$", "\\\\\\$")); |
| anchor.append("</a>"); |
| compoundLineLink(anchor, element); |
| anchor.append("<br>"); |
| } |
| } |
| else { |
| anchor.append("<font style=\"font-family:verdana; font-weight:bold; color:#FF0000\";>"); |
| anchor.append(InspectionsBundle.message("inspection.export.results.invalidated.item")); |
| anchor.append("</font>"); |
| } |
| } |
| |
| @SuppressWarnings({"HardCodedStringLiteral"}) |
| private static void compoundLineLink(StringBuffer lineAnchor, PsiElement psiElement) { |
| final PsiFile file = psiElement.getContainingFile(); |
| if (file != null) { |
| final VirtualFile vFile = file.getVirtualFile(); |
| if (vFile != null) { |
| Document doc = FileDocumentManager.getInstance().getDocument(vFile); |
| final int lineNumber = doc.getLineNumber(psiElement.getTextOffset()) + 1; |
| lineAnchor.append(" ").append(InspectionsBundle.message("inspection.export.results.at.line")).append(" "); |
| lineAnchor.append("<a HREF=\""); |
| try { |
| int offset = doc.getLineStartOffset(lineNumber - 1); |
| offset = CharArrayUtil.shiftForward(doc.getCharsSequence(), offset, " \t"); |
| lineAnchor.append(new URL(vFile.getUrl() + "#" + offset)); |
| } |
| catch (MalformedURLException e) { |
| LOG.error(e); |
| } |
| lineAnchor.append("\">"); |
| lineAnchor.append(Integer.toString(lineNumber)); |
| lineAnchor.append("</a>"); |
| } |
| } |
| } |
| |
| private void checkFile(final PsiFile file, |
| final InspectionManager manager, |
| GlobalInspectionContextBase context, |
| final RefManager refManager, |
| final ProblemDescriptionsProcessor processor) { |
| if (!(file instanceof PropertiesFile)) return; |
| if (!context.isToCheckFile(file, this)) return; |
| final PsiSearchHelper searchHelper = PsiSearchHelper.SERVICE.getInstance(file.getProject()); |
| final PropertiesFile propertiesFile = (PropertiesFile)file; |
| final List<IProperty> properties = propertiesFile.getProperties(); |
| Module module = ModuleUtilCore.findModuleForPsiElement(file); |
| if (module == null) return; |
| final GlobalSearchScope scope = CURRENT_FILE |
| ? GlobalSearchScope.fileScope(file) |
| : MODULE_WITH_DEPENDENCIES |
| ? GlobalSearchScope.moduleWithDependenciesScope(module) |
| : GlobalSearchScope.projectScope(file.getProject()); |
| final Map<String, Set<PsiFile>> processedValueToFiles = Collections.synchronizedMap(new HashMap<String, Set<PsiFile>>()); |
| final Map<String, Set<PsiFile>> processedKeyToFiles = Collections.synchronizedMap(new HashMap<String, Set<PsiFile>>()); |
| final ProgressIndicator original = ProgressManager.getInstance().getProgressIndicator(); |
| final ProgressIndicator progress = ProgressWrapper.wrap(original); |
| ProgressManager.getInstance().runProcess(new Runnable() { |
| @Override |
| public void run() { |
| if (!JobLauncher.getInstance().invokeConcurrentlyUnderProgress(properties, progress, false, new Processor<IProperty>() { |
| @Override |
| public boolean process(final IProperty property) { |
| if (original != null) { |
| if (original.isCanceled()) return false; |
| original.setText2(PropertiesBundle.message("searching.for.property.key.progress.text", property.getUnescapedKey())); |
| } |
| processTextUsages(processedValueToFiles, property.getValue(), processedKeyToFiles, searchHelper, scope); |
| processTextUsages(processedKeyToFiles, property.getUnescapedKey(), processedValueToFiles, searchHelper, scope); |
| return true; |
| } |
| })) throw new ProcessCanceledException(); |
| |
| List<ProblemDescriptor> problemDescriptors = new ArrayList<ProblemDescriptor>(); |
| Map<String, Set<String>> keyToDifferentValues = new HashMap<String, Set<String>>(); |
| if (CHECK_DUPLICATE_KEYS || CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES) { |
| prepareDuplicateKeysByFile(processedKeyToFiles, manager, keyToDifferentValues, problemDescriptors, file, original); |
| } |
| if (CHECK_DUPLICATE_VALUES) prepareDuplicateValuesByFile(processedValueToFiles, manager, problemDescriptors, file, original); |
| if (CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES) { |
| processDuplicateKeysWithDifferentValues(keyToDifferentValues, processedKeyToFiles, problemDescriptors, manager, file, original); |
| } |
| if (!problemDescriptors.isEmpty()) { |
| processor.addProblemElement(refManager.getReference(file), |
| problemDescriptors.toArray(new ProblemDescriptor[problemDescriptors.size()])); |
| } |
| } |
| }, progress); |
| } |
| |
| private static void processTextUsages(final Map<String, Set<PsiFile>> processedTextToFiles, |
| final String text, |
| final Map<String, Set<PsiFile>> processedFoundTextToFiles, |
| final PsiSearchHelper searchHelper, |
| final GlobalSearchScope scope) { |
| if (!processedTextToFiles.containsKey(text)) { |
| if (processedFoundTextToFiles.containsKey(text)) { |
| final Set<PsiFile> filesWithValue = processedFoundTextToFiles.get(text); |
| processedTextToFiles.put(text, filesWithValue); |
| } |
| else { |
| final Set<PsiFile> resultFiles = new HashSet<PsiFile>(); |
| findFilesWithText(text, searchHelper, scope, resultFiles); |
| if (resultFiles.isEmpty()) return; |
| processedTextToFiles.put(text, resultFiles); |
| } |
| } |
| } |
| |
| |
| private static void prepareDuplicateValuesByFile(final Map<String, Set<PsiFile>> valueToFiles, |
| final InspectionManager manager, |
| final List<ProblemDescriptor> problemDescriptors, |
| final PsiFile psiFile, |
| final ProgressIndicator progress) { |
| for (String value : valueToFiles.keySet()) { |
| if (progress != null){ |
| progress.setText2(InspectionsBundle.message("duplicate.property.value.progress.indicator.text", value)); |
| progress.checkCanceled(); |
| } |
| if (value.length() == 0) continue; |
| StringSearcher searcher = new StringSearcher(value, true, true); |
| StringBuffer message = new StringBuffer(); |
| int duplicatesCount = 0; |
| Set<PsiFile> psiFilesWithDuplicates = valueToFiles.get(value); |
| for (PsiFile file : psiFilesWithDuplicates) { |
| CharSequence text = file.getViewProvider().getContents(); |
| final char[] textArray = CharArrayUtil.fromSequenceWithoutCopying(text); |
| |
| for (int offset = LowLevelSearchUtil.searchWord(text, textArray, 0, text.length(), searcher, progress); |
| offset >= 0; |
| offset = LowLevelSearchUtil.searchWord(text, textArray, offset + searcher.getPattern().length(), text.length(), searcher, progress) |
| ) { |
| PsiElement element = file.findElementAt(offset); |
| if (element != null && element.getParent() instanceof Property) { |
| final Property property = (Property)element.getParent(); |
| if (Comparing.equal(property.getValue(), value) && element.getStartOffsetInParent() != 0) { |
| if (duplicatesCount == 0){ |
| message.append(InspectionsBundle.message("duplicate.property.value.problem.descriptor", property.getValue())); |
| } |
| surroundWithHref(message, element, true); |
| duplicatesCount ++; |
| } |
| } |
| } |
| } |
| if (duplicatesCount > 1) { |
| problemDescriptors.add(manager.createProblemDescriptor(psiFile, message.toString(), false, null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING)); |
| } |
| } |
| |
| |
| } |
| |
| private void prepareDuplicateKeysByFile(final Map<String, Set<PsiFile>> keyToFiles, |
| final InspectionManager manager, |
| final Map<String, Set<String>> keyToValues, |
| final List<ProblemDescriptor> problemDescriptors, |
| final PsiFile psiFile, |
| final ProgressIndicator progress) { |
| for (String key : keyToFiles.keySet()) { |
| if (progress!= null){ |
| progress.setText2(InspectionsBundle.message("duplicate.property.key.progress.indicator.text", key)); |
| if (progress.isCanceled()) throw new ProcessCanceledException(); |
| } |
| final StringBuffer message = new StringBuffer(); |
| int duplicatesCount = 0; |
| Set<PsiFile> psiFilesWithDuplicates = keyToFiles.get(key); |
| for (PsiFile file : psiFilesWithDuplicates) { |
| if (!(file instanceof PropertiesFile)) continue; |
| PropertiesFile propertiesFile = (PropertiesFile)file; |
| final List<IProperty> propertiesByKey = propertiesFile.findPropertiesByKey(key); |
| for (IProperty property : propertiesByKey) { |
| if (duplicatesCount == 0){ |
| message.append(InspectionsBundle.message("duplicate.property.key.problem.descriptor", key)); |
| } |
| surroundWithHref(message, property.getPsiElement().getFirstChild(), false); |
| duplicatesCount ++; |
| //prepare for filter same keys different values |
| Set<String> values = keyToValues.get(key); |
| if (values == null){ |
| values = new HashSet<String>(); |
| keyToValues.put(key, values); |
| } |
| values.add(property.getValue()); |
| } |
| } |
| if (duplicatesCount > 1 && CHECK_DUPLICATE_KEYS) { |
| problemDescriptors.add(manager.createProblemDescriptor(psiFile, message.toString(), false, null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING)); |
| } |
| } |
| |
| } |
| |
| |
| private static void processDuplicateKeysWithDifferentValues(final Map<String, Set<String>> keyToDifferentValues, |
| final Map<String, Set<PsiFile>> keyToFiles, |
| final List<ProblemDescriptor> problemDescriptors, |
| final InspectionManager manager, |
| final PsiFile psiFile, |
| final ProgressIndicator progress) { |
| for (String key : keyToDifferentValues.keySet()) { |
| if (progress != null) { |
| progress.setText2(InspectionsBundle.message("duplicate.property.diff.key.progress.indicator.text", key)); |
| if (progress.isCanceled()) throw new ProcessCanceledException(); |
| } |
| final Set<String> values = keyToDifferentValues.get(key); |
| if (values == null || values.size() < 2){ |
| keyToFiles.remove(key); |
| } else { |
| StringBuffer message = new StringBuffer(); |
| final Set<PsiFile> psiFiles = keyToFiles.get(key); |
| boolean firstUsage = true; |
| for (PsiFile file : psiFiles) { |
| if (!(file instanceof PropertiesFile)) continue; |
| PropertiesFile propertiesFile = (PropertiesFile)file; |
| final List<IProperty> propertiesByKey = propertiesFile.findPropertiesByKey(key); |
| for (IProperty property : propertiesByKey) { |
| if (firstUsage){ |
| message.append(InspectionsBundle.message("duplicate.property.diff.key.problem.descriptor", key)); |
| firstUsage = false; |
| } |
| surroundWithHref(message, property.getPsiElement().getFirstChild(), false); |
| } |
| } |
| problemDescriptors.add(manager.createProblemDescriptor(psiFile, message.toString(), false, null, ProblemHighlightType.GENERIC_ERROR_OR_WARNING)); |
| } |
| } |
| } |
| |
| private static void findFilesWithText(String stringToFind, |
| PsiSearchHelper searchHelper, |
| GlobalSearchScope scope, |
| final Set<PsiFile> resultFiles) { |
| final List<String> words = StringUtil.getWordsIn(stringToFind); |
| if (words.isEmpty()) return; |
| Collections.sort(words, new Comparator<String>() { |
| @Override |
| public int compare(final String o1, final String o2) { |
| return o2.length() - o1.length(); |
| } |
| }); |
| for (String word : words) { |
| final Set<PsiFile> files = new THashSet<PsiFile>(); |
| searchHelper.processAllFilesWithWord(word, scope, new CommonProcessors.CollectProcessor<PsiFile>(files), true); |
| if (resultFiles.isEmpty()) { |
| resultFiles.addAll(files); |
| } |
| else { |
| resultFiles.retainAll(files); |
| } |
| if (resultFiles.isEmpty()) return; |
| } |
| } |
| |
| @Override |
| @NotNull |
| public String getDisplayName() { |
| return InspectionsBundle.message("duplicate.property.display.name"); |
| } |
| |
| @Override |
| @NotNull |
| public String getGroupDisplayName() { |
| return InspectionsBundle.message("group.names.properties.files"); |
| } |
| |
| @Override |
| @NotNull |
| public String getShortName() { |
| return "DuplicatePropertyInspection"; |
| } |
| |
| @Override |
| public boolean isEnabledByDefault() { |
| return false; |
| } |
| |
| @Override |
| public JComponent createOptionsPanel() { |
| return new OptionsPanel().myWholePanel; |
| } |
| |
| public class OptionsPanel { |
| private JRadioButton myFileScope; |
| private JRadioButton myModuleScope; |
| private JRadioButton myProjectScope; |
| private JCheckBox myDuplicateValues; |
| private JCheckBox myDuplicateKeys; |
| private JCheckBox myDuplicateBoth; |
| private JPanel myWholePanel; |
| |
| OptionsPanel() { |
| ButtonGroup buttonGroup = new ButtonGroup(); |
| buttonGroup.add(myFileScope); |
| buttonGroup.add(myModuleScope); |
| buttonGroup.add(myProjectScope); |
| |
| myFileScope.setSelected(CURRENT_FILE); |
| myModuleScope.setSelected(MODULE_WITH_DEPENDENCIES); |
| myProjectScope.setSelected(!(CURRENT_FILE || MODULE_WITH_DEPENDENCIES)); |
| |
| myFileScope.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| CURRENT_FILE = myFileScope.isSelected(); |
| } |
| }); |
| myModuleScope.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| MODULE_WITH_DEPENDENCIES = myModuleScope.isSelected(); |
| if (MODULE_WITH_DEPENDENCIES) { |
| CURRENT_FILE = false; |
| } |
| } |
| }); |
| myProjectScope.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| if (myProjectScope.isSelected()) { |
| CURRENT_FILE = false; |
| MODULE_WITH_DEPENDENCIES = false; |
| } |
| } |
| }); |
| |
| myDuplicateKeys.setSelected(CHECK_DUPLICATE_KEYS); |
| myDuplicateValues.setSelected(CHECK_DUPLICATE_VALUES); |
| myDuplicateBoth.setSelected(CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES); |
| |
| myDuplicateKeys.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| CHECK_DUPLICATE_KEYS = myDuplicateKeys.isSelected(); |
| } |
| }); |
| myDuplicateValues.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| CHECK_DUPLICATE_VALUES = myDuplicateValues.isSelected(); |
| } |
| }); |
| myDuplicateBoth.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| CHECK_DUPLICATE_KEYS_WITH_DIFFERENT_VALUES = myDuplicateBoth.isSelected(); |
| } |
| }); |
| } |
| } |
| } |