IDEA-118080 (ability to define custom headers for inspection)

This commit is contained in:
Roman Shevchenko
2015-06-14 13:40:08 +03:00
parent 3901c9aa15
commit 25053738f5
9 changed files with 212 additions and 164 deletions
+2 -2
View File
@@ -17,6 +17,6 @@
<orderEntry type="module" module-name="java-psi-api" />
<orderEntry type="module" module-name="java-impl" />
<orderEntry type="module" module-name="testFramework-java" scope="TEST" />
<orderEntry type="module" module-name="spellchecker" />
</component>
</module>
</module>
@@ -1,4 +1,3 @@
manifest.unexpected.token=Unexpected token
manifest.colon.expected=':' expected
manifest.whitespace.expected=Whitespace expected
manifest.header.expected=Header expected
@@ -11,4 +10,6 @@ inspection.group=Manifest
inspection.newline.message=Manifest file doesn't end with a final newline
inspection.newline.fix=Add newline
inspection.header.message=Header name is unknown or spelled incorrectly
inspection.header.fix=Change to ''{0}''
inspection.header.ui.label=Custom headers:
inspection.header.rename.fix=Change to ''{0}''
inspection.header.remember.fix=Add ''{0}'' to custom headers
@@ -1,81 +0,0 @@
/*
* Copyright (c) 2007-2009, Osmorc Development Team
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright notice, this list
* of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice, this
* list of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
* * Neither the name of 'Osmorc Development Team' nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jetbrains.lang.manifest.header;
import org.jetbrains.annotations.NotNull;
/**
* A match describes how good a header known to a particular header provider matches a given header.
* The name of the given header may contain typos and so there may be no perfect match. A perfect match will
* have a Levenshtein distance of 0. Worse matches will have greater Levenshtein distances.
*
* @author Robert F. Beeger (robert@beeger.net)
*/
public class HeaderNameMatch implements Comparable<HeaderNameMatch> {
private final int myDistance;
private final String myHeaderName;
public HeaderNameMatch(int distance, @NotNull String headerName) {
myDistance = distance;
myHeaderName = headerName;
}
public int getDistance() {
return myDistance;
}
public String getHeaderName() {
return myHeaderName;
}
/**
* Matches are compared based on their distance.
*/
@Override
public int compareTo(@NotNull HeaderNameMatch o) {
return getDistance() - o.getDistance();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
HeaderNameMatch that = (HeaderNameMatch)o;
return myDistance == that.myDistance && myHeaderName.equals(that.myHeaderName);
}
@Override
public int hashCode() {
int result = myDistance;
result = 31 * result + myHeaderName.hashCode();
return result;
}
}
@@ -27,19 +27,17 @@ package org.jetbrains.lang.manifest.header;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.util.NotNullLazyValue;
import com.intellij.openapi.util.text.LevenshteinDistance;
import com.intellij.psi.PsiReference;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.CaseInsensitiveStringHashingStrategy;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.lang.manifest.psi.Header;
import org.jetbrains.lang.manifest.psi.HeaderValuePart;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* @author Robert F. Beeger (robert@beeger.net)
@@ -53,7 +51,7 @@ public class HeaderParserRepository {
@NotNull
@Override
protected Map<String, HeaderParser> compute() {
Map<String, HeaderParser> map = ContainerUtil.newHashMap();
Map<String, HeaderParser> map = new THashMap<String, HeaderParser>(CaseInsensitiveStringHashingStrategy.INSTANCE);
for (HeaderParserProvider provider : Extensions.getExtensions(HeaderParserProvider.EP_NAME)) {
map.putAll(provider.getHeaderParsers());
}
@@ -66,23 +64,6 @@ public class HeaderParserRepository {
return myParsers.getValue().get(headerName);
}
@NotNull
public Collection<HeaderNameMatch> getMatches(@NotNull String headerName) {
HeaderParser parser = myParsers.getValue().get(headerName);
if (parser != null) {
return ContainerUtil.emptyList();
}
LevenshteinDistance distance = new LevenshteinDistance();
Set<HeaderNameMatch> result = new TreeSet<HeaderNameMatch>();
for (Map.Entry<String, HeaderParser> entry : myParsers.getValue().entrySet()) {
String otherName = entry.getKey();
int dist = distance.calculateMetrics(headerName, otherName);
result.add(new HeaderNameMatch(dist, otherName));
}
return result;
}
@NotNull
public Set<String> getAllHeaderNames() {
return myParsers.getValue().keySet();
@@ -0,0 +1,33 @@
/*
* Copyright 2000-2015 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 org.jetbrains.lang.manifest.highlighting;
import com.intellij.codeInspection.LocalQuickFixOnPsiElement;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.lang.manifest.ManifestBundle;
public abstract class AbstractManifestQuickFix extends LocalQuickFixOnPsiElement {
protected AbstractManifestQuickFix(@NotNull PsiElement element) {
super(element);
}
@NotNull
@Override
public final String getFamilyName() {
return ManifestBundle.message("inspection.group");
}
}
@@ -61,7 +61,7 @@ public class MissingFinalNewlineInspection extends LocalInspectionTool {
return null;
}
private static class AddNewlineQuickFix extends LocalQuickFixOnPsiElement {
private static class AddNewlineQuickFix extends AbstractManifestQuickFix {
private AddNewlineQuickFix(Section section) {
super(section);
}
@@ -72,12 +72,6 @@ public class MissingFinalNewlineInspection extends LocalInspectionTool {
return ManifestBundle.message("inspection.newline.fix");
}
@NotNull
@Override
public String getFamilyName() {
return ManifestBundle.message("inspection.group");
}
@Override
public void invoke(@NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement, @NotNull PsiElement endElement) {
PsiElement lastChild = startElement.getLastChild();
@@ -26,25 +26,42 @@ package org.jetbrains.lang.manifest.highlighting;
import com.intellij.codeInspection.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.profile.codeInspection.InspectionProfileManager;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiFile;
import com.intellij.spellchecker.engine.Suggestion;
import com.intellij.ui.DocumentAdapter;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.CaseInsensitiveStringHashingStrategy;
import com.intellij.util.text.EditDistance;
import com.intellij.util.xmlb.annotations.AbstractCollection;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.lang.manifest.ManifestBundle;
import org.jetbrains.lang.manifest.header.HeaderNameMatch;
import org.jetbrains.lang.manifest.header.HeaderParserRepository;
import org.jetbrains.lang.manifest.psi.Header;
import java.util.Collection;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import java.awt.*;
import java.util.*;
import java.util.List;
/**
* @author Robert F. Beeger (robert@beeger.net)
*/
public class MisspelledHeaderInspection extends LocalInspectionTool {
private static final int MAX_SUGGESTIONS = 10;
private static final int MAX_SUGGESTIONS = 5;
private static final int MAX_DISTANCE = 4;
private static final int TYPO_DISTANCE = 2;
private HeaderParserRepository myRepository;
@AbstractCollection(surroundWithTag = false, elementTag = "header")
public final Set<String> CUSTOM_HEADERS = new THashSet<String>(CaseInsensitiveStringHashingStrategy.INSTANCE);
private final HeaderParserRepository myRepository;
public MisspelledHeaderInspection() {
myRepository = HeaderParserRepository.getInstance();
@@ -58,49 +75,117 @@ public class MisspelledHeaderInspection extends LocalInspectionTool {
public void visitElement(PsiElement element) {
if (element instanceof Header) {
Header header = (Header)element;
Collection<HeaderNameMatch> matches = myRepository.getMatches(header.getName());
if (!matches.isEmpty()) {
List<HeaderNameSpellingQuickFix> fixes = ContainerUtil.newArrayListWithCapacity(MAX_SUGGESTIONS);
for (HeaderNameMatch match : matches) {
fixes.add(new HeaderNameSpellingQuickFix(header, match));
if (fixes.size() == MAX_SUGGESTIONS) {
break;
}
}
holder.registerProblem(
header.getNameElement(), ManifestBundle.message("inspection.header.message"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, fixes.toArray(new HeaderNameSpellingQuickFix[fixes.size()])
);
String headerName = header.getName();
SortedSet<Suggestion> matches = new TreeSet<Suggestion>();
addMatches(headerName, CUSTOM_HEADERS, matches);
addMatches(headerName, myRepository.getAllHeaderNames(), matches);
Suggestion bestMatch = ContainerUtil.getFirstItem(matches);
if (bestMatch != null && headerName.equals(bestMatch.getWord())) {
return;
}
List<LocalQuickFix> fixes = new ArrayList<LocalQuickFix>();
for (Suggestion match : matches) {
fixes.add(new HeaderRenameQuickFix(header, match.getWord()));
if (fixes.size() == MAX_SUGGESTIONS) break;
}
if (bestMatch == null || bestMatch.getMetrics() > TYPO_DISTANCE) {
fixes.add(new CustomHeaderQuickFix(header, CUSTOM_HEADERS));
}
holder.registerProblem(
header.getNameElement(), ManifestBundle.message("inspection.header.message"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, fixes.toArray(new LocalQuickFix[fixes.size()])
);
}
}
private void addMatches(String headerName, Collection<String> headers, SortedSet<Suggestion> matches) {
for (String candidate : headers) {
int distance = EditDistance.optimalAlignment(headerName, candidate, false);
if (distance <= MAX_DISTANCE) {
matches.add(new Suggestion(candidate, distance));
}
}
}
};
}
private static class HeaderNameSpellingQuickFix implements LocalQuickFix {
private final Header myHeader;
@Override
public JComponent createOptionsPanel() {
return new OptionsPanel(CUSTOM_HEADERS);
}
private static class OptionsPanel extends JPanel {
public OptionsPanel(final Set<String> headers) {
super(new BorderLayout(5, 5));
add(new JLabel(ManifestBundle.message("inspection.header.ui.label")), BorderLayout.NORTH);
final JTextArea area = new JTextArea("");
add(area, BorderLayout.CENTER);
if (!headers.isEmpty()) {
area.setText(StringUtil.join(new TreeSet<String>(headers), "\n"));
}
area.getDocument().addDocumentListener(new DocumentAdapter() {
@Override
protected void textChanged(DocumentEvent e) {
headers.clear();
for (String line : StringUtil.split(area.getText(), "\n")) {
String header = line.trim();
if (!header.isEmpty()) {
headers.add(header);
}
}
}
});
}
}
private static class HeaderRenameQuickFix extends AbstractManifestQuickFix {
private final String myNewName;
private HeaderNameSpellingQuickFix(Header header, HeaderNameMatch match) {
myHeader = header;
myNewName = match.getHeaderName();
private HeaderRenameQuickFix(Header header, String newName) {
super(header);
myNewName = newName;
}
@NotNull
@Override
public String getName() {
return ManifestBundle.message("inspection.header.fix", myNewName);
public String getText() {
return ManifestBundle.message("inspection.header.rename.fix", myNewName);
}
@Override
public void invoke(@NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement, @NotNull PsiElement endElement) {
((Header)startElement).setName(myNewName);
}
}
private static class CustomHeaderQuickFix extends AbstractManifestQuickFix {
private final String myHeaderName;
private final Collection<String> myHeaders;
private CustomHeaderQuickFix(Header header, Collection<String> headers) {
super(header);
myHeaderName = header.getName();
myHeaders = headers;
}
@NotNull
@Override
public String getFamilyName() {
return ManifestBundle.message("inspection.group");
public String getText() {
return ManifestBundle.message("inspection.header.remember.fix", myHeaderName);
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
myHeader.setName(myNewName);
public void invoke(@NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement, @NotNull PsiElement endElement) {
myHeaders.add(myHeaderName);
InspectionProfile profile = InspectionProjectProfileManager.getInstance(project).getInspectionProfile();
InspectionProfileManager.getInstance().fireProfileChanged(profile);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
* Copyright 2000-2015 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.
@@ -18,7 +18,6 @@ package org.jetbrains.lang.manifest;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.LightIdeaTestCase;
import com.intellij.testFramework.LightPlatformTestCase;
import junit.framework.Assert;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.lang.manifest.psi.Header;
import org.jetbrains.lang.manifest.psi.HeaderValue;
@@ -27,16 +26,16 @@ import org.jetbrains.lang.manifest.psi.ManifestFile;
public class ManifestPsiTest extends LightIdeaTestCase {
public void testFile() {
ManifestFile file = createFile("");
Assert.assertEquals(0, file.getSections().size());
Assert.assertNull(file.getMainSection());
Assert.assertEquals(0, file.getHeaders().size());
assertEquals(0, file.getSections().size());
assertNull(file.getMainSection());
assertEquals(0, file.getHeaders().size());
file = createFile("Header: value\n\nAnother-Header: another value\n");
Assert.assertEquals(2, file.getSections().size());
Assert.assertNotNull(file.getMainSection());
Assert.assertEquals(1, file.getHeaders().size());
Assert.assertNotNull(file.getHeader("Header"));
Assert.assertNull(file.getHeader("Another-Header"));
assertEquals(2, file.getSections().size());
assertNotNull(file.getMainSection());
assertEquals(1, file.getHeaders().size());
assertNotNull(file.getHeader("Header"));
assertNull(file.getHeader("Another-Header"));
}
public void testHeader() {
@@ -54,15 +53,15 @@ public class ManifestPsiTest extends LightIdeaTestCase {
private static void assertHeaderValue(ManifestFile file, String name, @Nullable String expected) {
Header header = file.getHeader(name);
Assert.assertNotNull(header);
assertNotNull(header);
HeaderValue value = header.getHeaderValue();
if (expected == null) {
Assert.assertNull(value);
assertNull(value);
}
else {
Assert.assertNotNull(value);
Assert.assertEquals(expected, value.getUnwrappedText());
assertNotNull(value);
assertEquals(expected, value.getUnwrappedText());
}
}
}
@@ -19,25 +19,61 @@ import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
import org.jetbrains.lang.manifest.highlighting.MisspelledHeaderInspection;
import java.util.Collections;
import java.util.List;
public class MisspelledHeaderInspectionTest extends LightCodeInsightFixtureTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
myFixture.enableInspections(new MisspelledHeaderInspection());
public void testNoProblem() {
doTest("Manifest-Version: 1.0", 0);
}
public void testNoProblem() {
myFixture.configureByText(ManifestFileTypeFactory.MANIFEST, "Manifest-Version: 1.0\n");
assertEquals(0, myFixture.getAvailableIntentions().size());
public void testMixedCase() {
doTest("<weak_warning descr=\"Header name is unknown or spelled incorrectly\">manifest-version</weak_warning>: 1.0", 1);
}
public void testMissedDash() {
doTest("<weak_warning descr=\"Header name is unknown or spelled incorrectly\">ManifestVersion</weak_warning>: 1.0", 1);
}
public void testMisspelled() {
doTest("<weak_warning descr=\"Header name is unknown or spelled incorrectly\">MainFestVersion</weak_warning>: 1.0", 1);
}
public void testTotallyIncorrect() {
doTest("<weak_warning descr=\"Header name is unknown or spelled incorrectly\">some_totally_impossible_header</weak_warning>: -", 0);
}
public void testFix() {
myFixture.enableInspections(new MisspelledHeaderInspection());
myFixture.configureByText(ManifestFileTypeFactory.MANIFEST, "ManifestVersion: 1.0\n");
List<IntentionAction> intentions = myFixture.filterAvailableIntentions("Change to");
assertTrue(intentions.size() > 0);
assertEquals(1, intentions.size());
myFixture.launchAction(intentions.get(0));
myFixture.checkResult("Manifest-Version: 1.0\n");
}
public void testCustomHeader() {
MisspelledHeaderInspection inspection = new MisspelledHeaderInspection();
inspection.CUSTOM_HEADERS.add("Custom-Header");
myFixture.enableInspections(inspection);
myFixture.configureByText(ManifestFileTypeFactory.MANIFEST, "Custom-Header: -\n");
myFixture.checkHighlighting();
}
public void testCustomHeaderFix() {
MisspelledHeaderInspection inspection = new MisspelledHeaderInspection();
myFixture.enableInspections(inspection);
myFixture.configureByText(ManifestFileTypeFactory.MANIFEST, "Custom-Header: -\n");
List<IntentionAction> intentions = myFixture.filterAvailableIntentions("Add ");
assertEquals(1, intentions.size());
myFixture.launchAction(intentions.get(0));
assertEquals(Collections.singleton("Custom-Header"), inspection.CUSTOM_HEADERS);
}
private void doTest(String text, int expected) {
myFixture.enableInspections(new MisspelledHeaderInspection());
myFixture.configureByText(ManifestFileTypeFactory.MANIFEST, text + "\n");
myFixture.checkHighlighting();
assertEquals(expected, myFixture.filterAvailableIntentions("Change to").size());
}
}