From 798d79706163b0eaf7f6e095aa72f02513b4e7af Mon Sep 17 00:00:00 2001 From: Bas Leijdekkers Date: Wed, 1 Mar 2017 13:33:53 +0100 Subject: [PATCH] new "Missing closing tag" HTML inspection --- .../src/messages/XmlBundle.properties | 1 + .../src/META-INF/XmlPlugin.xml | 4 + .../HtmlMissingClosingTagInspectionTest.java | 88 +++++++++++ .../HtmlMissingClosingTag.html | 8 + .../HtmlMissingClosingTagInspection.java | 148 ++++++++++++++++++ .../messages/XmlErrorMessages.properties | 3 + 6 files changed, 252 insertions(+) create mode 100644 xml/tests/src/com/intellij/codeInspection/htmlInspections/HtmlMissingClosingTagInspectionTest.java create mode 100644 xml/xml-analysis-impl/resources/inspectionDescriptions/HtmlMissingClosingTag.html create mode 100644 xml/xml-analysis-impl/src/com/intellij/codeInspection/htmlInspections/HtmlMissingClosingTagInspection.java diff --git a/platform/platform-resources-en/src/messages/XmlBundle.properties b/platform/platform-resources-en/src/messages/XmlBundle.properties index 1d32d312c74d..20413afb3596 100644 --- a/platform/platform-resources-en/src/messages/XmlBundle.properties +++ b/platform/platform-resources-en/src/messages/XmlBundle.properties @@ -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} diff --git a/platform/platform-resources/src/META-INF/XmlPlugin.xml b/platform/platform-resources/src/META-INF/XmlPlugin.xml index 30e54eae7e9e..3eb580b846a6 100644 --- a/platform/platform-resources/src/META-INF/XmlPlugin.xml +++ b/platform/platform-resources/src/META-INF/XmlPlugin.xml @@ -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"/> + "); + } + + public void testIncompleteElement() { + highlightTest("</html>"); + } + + public void testPElement() { + quickfixTest("< is missing an end tag\">p>Behold!", "

Behold!

", "Add

"); + } + + + + @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 allIntentions = myFixture.getAvailableIntentions(); + final List 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); + } +} diff --git a/xml/xml-analysis-impl/resources/inspectionDescriptions/HtmlMissingClosingTag.html b/xml/xml-analysis-impl/resources/inspectionDescriptions/HtmlMissingClosingTag.html new file mode 100644 index 000000000000..f13c9c9ed03c --- /dev/null +++ b/xml/xml-analysis-impl/resources/inspectionDescriptions/HtmlMissingClosingTag.html @@ -0,0 +1,8 @@ + + +Reports HTML elements without a closing tag. +Some coding styles require a closing tag on HTML elements even where this is optional. + +

New in 2017.2 + + \ No newline at end of file diff --git a/xml/xml-analysis-impl/src/com/intellij/codeInspection/htmlInspections/HtmlMissingClosingTagInspection.java b/xml/xml-analysis-impl/src/com/intellij/codeInspection/htmlInspections/HtmlMissingClosingTagInspection.java new file mode 100644 index 000000000000..2ed1afae1abe --- /dev/null +++ b/xml/xml-analysis-impl/src/com/intellij/codeInspection/htmlInspections/HtmlMissingClosingTagInspection.java @@ -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 + ""; + 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("" + grandChild.getText() + ""); + final XmlText whitespace = PsiTreeUtil.getChildOfType(dummyTag, XmlText.class); + assert whitespace != null; + replacement.getParent().addAfter(whitespace, replacement); + } + } + }); + } + } +} diff --git a/xml/xml-psi-impl/resources/messages/XmlErrorMessages.properties b/xml/xml-psi-impl/resources/messages/XmlErrorMessages.properties index 2166feff1bad..605b2b9a7865 100644 --- a/xml/xml-psi-impl/resources/messages/XmlErrorMessages.properties +++ b/xml/xml-psi-impl/resources/messages/XmlErrorMessages.properties @@ -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 \\<#ref\\> 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 element.is.not.closed=Element is not closed named.element.is.not.closed=Element {0} is not closed