new "Missing closing tag" HTML inspection

This commit is contained in:
Bas Leijdekkers
2017-03-02 11:36:09 +01:00
parent 06fd8e5874
commit 798d797061
6 changed files with 252 additions and 0 deletions
@@ -82,6 +82,7 @@ html.inspections.unknown.tag.attribute.checkbox.title=Custom HTML tag attributes
html.inspections.unknown.tag.boolean.attribute.checkbox.title=Custom HTML boolean tag attributes:
html.inspections.unknown.tag.attribute.title=Edit custom attributes
html.inspections.unknown.tag.boolean.attribute.title=Edit custom boolean attributes
html.inspections.missing.closing.tag=Missing closing tag
xml.schema.create.complex.type.intention.name=Create Complex Type {0}
xml.schema.create.attribute.intention.name=Create Attribute {0}
xml.schema.create.element.intention.name=Create Element {0}
@@ -460,6 +460,10 @@
bundle="messages.XmlBundle" key="html.inspections.unknown.boolean.attribute"
groupBundle="messages.XmlBundle" groupKey="html.inspections.group.name"
implementationClass="com.intellij.codeInspection.htmlInspections.HtmlUnknownBooleanAttributeInspection"/>
<localInspection language="HTML" enabledByDefault="true" level="INFORMATION"
bundle="messages.XmlBundle" key="html.inspections.missing.closing.tag"
groupBundle="messages.XmlBundle" groupKey="html.inspections.group.name"
implementationClass="com.intellij.codeInspection.htmlInspections.HtmlMissingClosingTagInspection"/>
<localInspection language="XML" shortName="HtmlUnknownTarget" enabledByDefault="true" level="WARNING"
bundle="messages.XmlBundle" key="html.inspections.unknown.target"
groupBundle="messages.XmlBundle" groupKey="html.inspections.group.name"
@@ -0,0 +1,88 @@
/*
* Copyright 2000-2017 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.htmlInspections;
import com.intellij.codeHighlighting.HighlightDisplayLevel;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.ex.InspectionProfileImpl;
import com.intellij.ide.highlighter.HtmlFileType;
import com.intellij.openapi.project.Project;
import com.intellij.profile.codeInspection.ProjectInspectionProfileManager;
import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Bas Leijdekkers
*/
public class HtmlMissingClosingTagInspectionTest extends LightPlatformCodeInsightFixtureTestCase {
public void testImgElement() {
highlightTest("<html><body><img></body></html>");
}
public void testIncompleteElement() {
highlightTest("<html><table><error descr=\"Element table is not closed\"><</error>/html>");
}
public void testPElement() {
quickfixTest("<html><<warning descr=\"Element <p> is missing an end tag\">p</warning><caret>>Behold!</html>", "<html><p>Behold!</p></html>", "Add </p>");
}
@NotNull
protected LocalInspectionTool getInspection() {
return new HtmlMissingClosingTagInspection();
}
protected void highlightTest(@Language("HTML") String code) {
final LocalInspectionTool inspection = getInspection();
myFixture.enableInspections(inspection);
final HighlightDisplayKey displayKey = HighlightDisplayKey.find(inspection.getShortName());
final Project project = myFixture.getProject();
final InspectionProfileImpl currentProfile = ProjectInspectionProfileManager.getInstance(project).getCurrentProfile();
final HighlightDisplayLevel errorLevel = currentProfile.getErrorLevel(displayKey, null);
if (errorLevel == HighlightDisplayLevel.DO_NOT_SHOW) {
currentProfile.setErrorLevel(displayKey, HighlightDisplayLevel.WARNING, project);
}
myFixture.configureByText(HtmlFileType.INSTANCE, code);
myFixture.testHighlighting();
}
protected void quickfixTest(@Language("HTML") String before, @Language("HTML") String after, String hint) {
highlightTest(before);
final IntentionAction intention = findIntention(hint);
assertNotNull(intention);
myFixture.launchAction(intention);
myFixture.checkResult(after);
}
protected IntentionAction findIntention(@NotNull final String hint) {
final List<IntentionAction> allIntentions = myFixture.getAvailableIntentions();
final List<IntentionAction> intentions =
allIntentions.stream().filter(action -> action.getText().startsWith(hint)).limit(2).collect(Collectors.toList());
Assert.assertFalse("\"" + hint + "\" not in " + intentions, intentions.isEmpty());
Assert.assertFalse("Too many quickfixes found for \"" + hint + "\": " + intentions + "]", intentions.size() > 1);
return intentions.get(0);
}
}
@@ -0,0 +1,8 @@
<html>
<body>
Reports HTML elements without a closing tag.
Some coding styles require a closing tag on HTML elements even where this is optional.
<!-- tooltip end -->
<p><small>New in 2017.2</small>
</body>
</html>
@@ -0,0 +1,148 @@
/*
* Copyright 2000-2017 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.htmlInspections;
import com.intellij.codeInsight.daemon.XmlErrorMessages;
import com.intellij.codeInsight.daemon.impl.analysis.XmlHighlightVisitor;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiErrorElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.XmlElementFactory;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.html.HtmlTag;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlText;
import com.intellij.psi.xml.XmlToken;
import com.intellij.psi.xml.XmlTokenType;
import com.intellij.xml.util.HtmlUtil;
import com.intellij.xml.util.XmlTagUtil;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
/**
* @author Bas Leijdekkers
*/
public class HtmlMissingClosingTagInspection extends HtmlLocalInspectionTool {
@Override
protected void checkTag(@NotNull XmlTag tag, @NotNull ProblemsHolder holder, boolean isOnTheFly) {
if (!(tag instanceof HtmlTag) || !XmlHighlightVisitor.shouldBeValidated(tag)) {
return;
}
PsiElement child = tag.getLastChild();
if (child instanceof PsiErrorElement) {
return;
}
final XmlToken tagNameElement = XmlTagUtil.getStartTagNameElement(tag);
if (tagNameElement == null) {
return;
}
final String tagName = tagNameElement.getText();
if (HtmlUtil.isSingleHtmlTag(tagName)) {
return;
}
if (isToken(child, XmlTokenType.XML_TAG_END)) {
child = child.getPrevSibling();
if (isToken(child, XmlTokenType.XML_NAME) && tagName.equals(child.getText())) {
child = child.getPrevSibling();
if (isToken(child, XmlTokenType.XML_END_TAG_START)) {
return;
}
}
}
holder.registerProblem(tagNameElement, XmlErrorMessages.message("element.missing.end.tag"),
new MissingClosingTagFix(tagName));
}
private static boolean isToken(PsiElement element, IElementType tokenType) {
if (!(element instanceof XmlToken)) {
return false;
}
final XmlToken token = (XmlToken)element;
return token.getTokenType() == tokenType;
}
private static class MissingClosingTagFix implements LocalQuickFix {
private final String myName;
public MissingClosingTagFix(String name) {
myName = name;
}
@Nls
@NotNull
@Override
public String getName() {
return XmlErrorMessages.message("add.named.closing.tag", myName);
}
@Nls
@NotNull
@Override
public String getFamilyName() {
return XmlErrorMessages.message("add.closing.tag");
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
final PsiElement element = descriptor.getPsiElement();
if (!(element instanceof XmlToken)) {
return;
}
final PsiElement parent = element.getParent();
if (!(parent instanceof XmlTag)) {
return;
}
String text = parent.getText();
if (text.contains("\n")) {
int cutoff = -1;
for (int i = text.length() - 1; i > 0; i--) {
final char c = text.charAt(i);
if (c == ' ' || c == '\t') continue;
if (c == '\n') cutoff = i;
else break;
}
if (cutoff > 0) {
text = text.substring(0, cutoff);
}
}
final String replacementText = text + "</" + element.getText() + ">";
final XmlElementFactory factory = XmlElementFactory.getInstance(project);
final XmlTag newTag = factory.createHTMLTagFromText(replacementText);
final PsiElement child = parent.getLastChild().copy();
CodeStyleManager.getInstance(project).performActionWithFormatterDisabled((Runnable)() -> {
final PsiElement replacement = parent.replace(newTag);
if (child instanceof XmlText) {
final PsiElement grandChild = child.getLastChild();
if (grandChild instanceof PsiWhiteSpace) {
final XmlTag dummyTag = factory.createHTMLTagFromText("<dummy>" + grandChild.getText() + "</dummy>");
final XmlText whitespace = PsiTreeUtil.getChildOfType(dummyTag, XmlText.class);
assert whitespace != null;
replacement.getParent().addAfter(whitespace, replacement);
}
}
});
}
}
}
@@ -22,6 +22,7 @@ invalid.id.reference=Invalid id reference
uri.is.not.registered=URI is not registered (Settings | Languages \\& Frameworks | Schemas and DTDs)
registered.resource.is.not.recognized=Resource registered by this uri is not recognized (Settings | Languages \\& Frameworks | Schemas and DTDs)
should.have.fixed.value={0} should have fixed value {1}
element.missing.end.tag=Element \\&lt;#ref\\&gt; is missing an end tag
#quickfixes
insert.required.attribute.quickfix.text=Insert required attribute {0}
@@ -37,6 +38,8 @@ select.namespace.title={0} To Import
select.namespace.location.title=Select Namespace Location
declare.id.in.comment.quickfix=Declare invalid id in comment annotation
switch.to.html5.quickfix.text=Switch to HTML5 language level
add.closing.tag=Add closing tag
add.named.closing.tag=Add </{0}>
element.is.not.closed=Element is not closed
named.element.is.not.closed=Element {0} is not closed