| /* |
| * Copyright 2000-2013 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.codeInsight.daemon.impl.analysis; |
| |
| import com.intellij.BundleBase; |
| import com.intellij.codeInsight.daemon.*; |
| import com.intellij.codeInsight.daemon.impl.HighlightInfo; |
| import com.intellij.codeInsight.daemon.impl.HighlightInfoType; |
| import com.intellij.codeInsight.daemon.impl.HighlightVisitor; |
| import com.intellij.codeInsight.daemon.impl.SeverityRegistrar; |
| import com.intellij.codeInsight.daemon.impl.quickfix.QuickFixAction; |
| import com.intellij.codeInsight.daemon.impl.quickfix.QuickFixActionRegistrarImpl; |
| import com.intellij.codeInsight.intention.IntentionAction; |
| import com.intellij.codeInsight.quickfix.UnresolvedReferenceQuickFixProvider; |
| import com.intellij.codeInspection.*; |
| import com.intellij.codeInspection.htmlInspections.RequiredAttributesInspectionBase; |
| import com.intellij.codeInspection.htmlInspections.XmlEntitiesInspection; |
| import com.intellij.lang.ASTNode; |
| import com.intellij.lang.dtd.DTDLanguage; |
| import com.intellij.lang.injection.InjectedLanguageManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.markup.TextAttributes; |
| import com.intellij.openapi.extensions.Extensions; |
| import com.intellij.openapi.progress.ProgressManager; |
| import com.intellij.openapi.util.Comparing; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.openapi.util.UserDataCache; |
| import com.intellij.profile.codeInspection.InspectionProjectProfileManager; |
| import com.intellij.psi.*; |
| import com.intellij.psi.html.HtmlTag; |
| import com.intellij.psi.impl.source.SourceTreeToPsiMap; |
| import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceOwner; |
| import com.intellij.psi.meta.PsiMetaData; |
| import com.intellij.psi.templateLanguages.OuterLanguageElement; |
| import com.intellij.psi.tree.IElementType; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.psi.xml.*; |
| import com.intellij.xml.XmlAttributeDescriptor; |
| import com.intellij.xml.XmlElementDescriptor; |
| import com.intellij.xml.XmlExtension; |
| import com.intellij.xml.XmlUndefinedElementFixProvider; |
| import com.intellij.xml.impl.schema.AnyXmlElementDescriptor; |
| import com.intellij.xml.util.AnchorReference; |
| import com.intellij.xml.util.HtmlUtil; |
| import com.intellij.xml.util.XmlTagUtil; |
| import com.intellij.xml.util.XmlUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.HashSet; |
| import java.util.Set; |
| import java.util.StringTokenizer; |
| |
| /** |
| * @author Mike |
| */ |
| public class XmlHighlightVisitor extends XmlElementVisitor implements HighlightVisitor, IdeValidationHost { |
| private static final Logger LOG = Logger.getInstance("com.intellij.codeInsight.daemon.impl.analysis.XmlHighlightVisitor"); |
| private static final UserDataCache<Boolean, PsiElement, Object> DO_NOT_VALIDATE = |
| new UserDataCache<Boolean, PsiElement, Object>("do not validate") { |
| @Override |
| protected Boolean compute(PsiElement parent, Object p) { |
| OuterLanguageElement element = PsiTreeUtil.getChildOfType(parent, OuterLanguageElement.class); |
| |
| if (element == null) { |
| // JspOuterLanguageElement is located under XmlText |
| for (PsiElement child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { |
| if (child instanceof XmlText) { |
| element = PsiTreeUtil.getChildOfType(child, OuterLanguageElement.class); |
| if (element != null) { |
| break; |
| } |
| } |
| } |
| } |
| if (element == null) return false; |
| PsiFile containingFile = parent.getContainingFile(); |
| return containingFile.getViewProvider().getBaseLanguage() != containingFile.getLanguage(); |
| } |
| }; |
| private static boolean ourDoJaxpTesting; |
| |
| private static final TextAttributes NONEMPTY_TEXT_ATTRIBUTES = new TextAttributes() { |
| @Override |
| public boolean isEmpty() { |
| return false; |
| } |
| }; |
| private HighlightInfoHolder myHolder; |
| |
| public XmlHighlightVisitor() { |
| } |
| |
| private void addElementsForTag(XmlTag tag, |
| @NotNull String localizedMessage, |
| HighlightInfoType type, |
| IntentionAction quickFixAction) { |
| addElementsForTagWithManyQuickFixes(tag, localizedMessage, type, quickFixAction); |
| } |
| |
| private void addElementsForTagWithManyQuickFixes(XmlTag tag, |
| @NotNull String localizedMessage, |
| HighlightInfoType type, IntentionAction... quickFixActions) { |
| bindMessageToTag(tag, type, -1, localizedMessage, quickFixActions); |
| } |
| |
| @Override public void visitXmlToken(XmlToken token) { |
| IElementType tokenType = token.getTokenType(); |
| if (tokenType == XmlTokenType.XML_NAME || tokenType == XmlTokenType.XML_TAG_NAME) { |
| PsiElement element = token.getPrevSibling(); |
| while(element instanceof PsiWhiteSpace) element = element.getPrevSibling(); |
| |
| if (element instanceof XmlToken) { |
| if (((XmlToken)element).getTokenType() == XmlTokenType.XML_START_TAG_START) { |
| PsiElement parent = element.getParent(); |
| |
| if (parent instanceof XmlTag && !(token.getNextSibling() instanceof OuterLanguageElement)) { |
| checkTag((XmlTag)parent); |
| } |
| } |
| } else { |
| PsiElement parent = token.getParent(); |
| |
| if (parent instanceof XmlAttribute && !(token.getNextSibling() instanceof OuterLanguageElement)) { |
| checkAttribute((XmlAttribute) parent); |
| } |
| } |
| } else if (tokenType == XmlTokenType.XML_DATA_CHARACTERS && token.getParent() instanceof XmlText) { |
| if (token.textContains(']') && token.textContains('>')) { |
| |
| String s = token.getText(); |
| String marker = "]]>"; |
| int i = s.indexOf(marker); |
| |
| if (i != -1 ) { // TODO: fix |
| XmlTag tag = PsiTreeUtil.getParentOfType(token, XmlTag.class); |
| if (tag != null && XmlExtension.getExtensionByElement(tag).shouldBeHighlightedAsTag(tag) && !skipValidation(tag)) { |
| TextRange textRange = token.getTextRange(); |
| int start = textRange.getStartOffset() + i; |
| HighlightInfoType type = tag instanceof HtmlTag ? HighlightInfoType.WARNING : HighlightInfoType.ERROR; |
| String description = XmlErrorMessages.message("cdata.end.should.not.appear.in.content.unless.to.mark.end.of.cdata.section"); |
| HighlightInfo info = HighlightInfo.newHighlightInfo(type).range(start, start + marker.length()).descriptionAndTooltip(description).create(); |
| addToResults(info); |
| } |
| } |
| } |
| } |
| } |
| |
| private void checkTag(XmlTag tag) { |
| if (ourDoJaxpTesting) return; |
| |
| if (!myHolder.hasErrorResults()) { |
| checkTagByDescriptor(tag); |
| } |
| |
| if (!myHolder.hasErrorResults()) { |
| if (!skipValidation(tag)) { |
| final XmlElementDescriptor descriptor = tag.getDescriptor(); |
| |
| if (tag instanceof HtmlTag && |
| ( descriptor instanceof AnyXmlElementDescriptor || |
| descriptor == null |
| ) |
| ) { |
| return; |
| } |
| |
| checkReferences(tag); |
| } |
| } |
| } |
| |
| private void bindMessageToTag(final XmlTag tag, |
| final HighlightInfoType warning, |
| final int messageLength, |
| @NotNull String localizedMessage, |
| IntentionAction... quickFixActions) { |
| XmlToken childByRole = XmlTagUtil.getStartTagNameElement(tag); |
| |
| bindMessageToAstNode(childByRole, warning, 0, messageLength, localizedMessage, quickFixActions); |
| childByRole = XmlTagUtil.getEndTagNameElement(tag); |
| bindMessageToAstNode(childByRole, warning, 0, messageLength, localizedMessage, quickFixActions); |
| } |
| |
| |
| @Override |
| public void visitXmlProcessingInstruction(XmlProcessingInstruction processingInstruction) { |
| super .visitXmlProcessingInstruction(processingInstruction); |
| PsiElement parent = processingInstruction.getParent(); |
| |
| if (parent instanceof XmlProlog && processingInstruction.getText().startsWith("<?xml")) { |
| for(PsiElement e = PsiTreeUtil.prevLeaf(processingInstruction); e != null; e = PsiTreeUtil.prevLeaf(e)) { |
| if (e instanceof PsiWhiteSpace && PsiTreeUtil.prevLeaf(e) != null || |
| e instanceof OuterLanguageElement) { |
| continue; |
| } |
| PsiElement eParent = e.getParent(); |
| if (eParent instanceof PsiComment) e = eParent; |
| if (eParent instanceof XmlProcessingInstruction) break; |
| |
| String description = XmlErrorMessages.message("xml.declaration.should.precede.all.document.content"); |
| addToResults(HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(e).descriptionAndTooltip(description).create()); |
| } |
| } |
| checkReferences(processingInstruction); |
| } |
| |
| private void bindMessageToAstNode(final PsiElement childByRole, |
| final HighlightInfoType warning, |
| final int offset, |
| int length, |
| @NotNull String localizedMessage, |
| IntentionAction... quickFixActions) { |
| if(childByRole != null) { |
| final TextRange textRange = childByRole.getTextRange(); |
| if (length == -1) length = textRange.getLength(); |
| final int startOffset = textRange.getStartOffset() + offset; |
| |
| HighlightInfo highlightInfo = HighlightInfo.newHighlightInfo(warning).range(childByRole, startOffset, startOffset + length).descriptionAndTooltip(localizedMessage).create(); |
| |
| if (highlightInfo == null) { |
| highlightInfo = HighlightInfo.newHighlightInfo(warning).range(new TextRange(startOffset, startOffset + length)).textAttributes(NONEMPTY_TEXT_ATTRIBUTES).descriptionAndTooltip(localizedMessage).create(); |
| } |
| |
| for (final IntentionAction quickFixAction : quickFixActions) { |
| if (quickFixAction == null) continue; |
| QuickFixAction.registerQuickFixAction(highlightInfo, textRange, quickFixAction); |
| } |
| addToResults(highlightInfo); |
| } |
| } |
| |
| private void checkTagByDescriptor(final XmlTag tag) { |
| String name = tag.getName(); |
| |
| XmlElementDescriptor elementDescriptor; |
| |
| final PsiElement parent = tag.getParent(); |
| if (parent instanceof XmlTag) { |
| XmlTag parentTag = (XmlTag)parent; |
| |
| elementDescriptor = XmlUtil.getDescriptorFromContext(tag); |
| |
| final XmlElementDescriptor parentDescriptor = parentTag.getDescriptor(); |
| |
| if (parentDescriptor != null && elementDescriptor == null && shouldBeValidated(tag)) { |
| if (tag instanceof HtmlTag) { |
| //XmlEntitiesInspection inspection = getInspectionProfile(tag, HtmlStyleLocalInspection.SHORT_NAME); |
| //if (inspection != null /*&& isAdditionallyDeclared(inspection.getAdditionalEntries(XmlEntitiesInspection.UNKNOWN_TAG), name)*/) { |
| return; |
| //} |
| } |
| |
| addElementsForTag( |
| tag, |
| XmlErrorMessages.message("element.is.not.allowed.here", name), |
| getTagProblemInfoType(tag), |
| null |
| ); |
| return; |
| } |
| |
| if (elementDescriptor instanceof AnyXmlElementDescriptor || |
| elementDescriptor == null |
| ) { |
| elementDescriptor = tag.getDescriptor(); |
| } |
| |
| if (elementDescriptor == null) return; |
| } |
| else { |
| //root tag |
| elementDescriptor = tag.getDescriptor(); |
| |
| if (elementDescriptor == null) { |
| addElementsForTag(tag, XmlErrorMessages.message("element.must.be.declared", name), HighlightInfoType.WRONG_REF, null); |
| return; |
| } |
| } |
| |
| if (!(elementDescriptor instanceof XmlHighlightingAwareElementDescriptor) || |
| ((XmlHighlightingAwareElementDescriptor)elementDescriptor).shouldCheckRequiredAttributes()) { |
| checkRequiredAttributes(tag, name, elementDescriptor); |
| } |
| |
| if (elementDescriptor instanceof Validator) { |
| //noinspection unchecked |
| ((Validator<XmlTag>)elementDescriptor).validate(tag,this); |
| } |
| } |
| |
| private void checkRequiredAttributes(XmlTag tag, String name, XmlElementDescriptor elementDescriptor) { |
| XmlAttributeDescriptor[] attributeDescriptors = elementDescriptor.getAttributesDescriptors(tag); |
| Set<String> requiredAttributes = null; |
| |
| for (XmlAttributeDescriptor attribute : attributeDescriptors) { |
| if (attribute != null && attribute.isRequired()) { |
| if (requiredAttributes == null) { |
| requiredAttributes = new HashSet<String>(); |
| } |
| requiredAttributes.add(attribute.getName(tag)); |
| } |
| } |
| |
| if (requiredAttributes != null) { |
| for (final String attrName : requiredAttributes) { |
| if (tag.getAttributeValue(attrName) == null && |
| !XmlExtension.getExtension(tag.getContainingFile()).isRequiredAttributeImplicitlyPresent(tag, attrName)) { |
| |
| IntentionAction insertRequiredAttributeIntention = XmlQuickFixFactory.getInstance().insertRequiredAttributeFix(tag, attrName); |
| final String localizedMessage = XmlErrorMessages.message("element.doesnt.have.required.attribute", name, attrName); |
| final InspectionProfile profile = InspectionProjectProfileManager.getInstance(tag.getProject()).getInspectionProfile(); |
| RequiredAttributesInspectionBase inspection = |
| (RequiredAttributesInspectionBase)profile.getUnwrappedTool(XmlEntitiesInspection.REQUIRED_ATTRIBUTES_SHORT_NAME, tag); |
| if (inspection != null) { |
| reportOneTagProblem( |
| tag, |
| attrName, |
| localizedMessage, |
| insertRequiredAttributeIntention, |
| HighlightDisplayKey.find(XmlEntitiesInspection.REQUIRED_ATTRIBUTES_SHORT_NAME), |
| inspection, |
| RequiredAttributesInspectionBase.getIntentionAction(attrName) |
| ); |
| } |
| } |
| } |
| } |
| } |
| |
| private void reportOneTagProblem(final XmlTag tag, |
| final String name, |
| @NotNull String localizedMessage, |
| final IntentionAction basicIntention, |
| final HighlightDisplayKey key, |
| final XmlEntitiesInspection inspection, |
| final IntentionAction addAttributeFix) { |
| boolean htmlTag = false; |
| |
| if (tag instanceof HtmlTag) { |
| htmlTag = true; |
| if(isAdditionallyDeclared(inspection.getAdditionalEntries(), name)) return; |
| } |
| |
| final InspectionProfile profile = InspectionProjectProfileManager.getInstance(tag.getProject()).getInspectionProfile(); |
| if (htmlTag && profile.isToolEnabled(key, tag)) { |
| addElementsForTagWithManyQuickFixes( |
| tag, |
| localizedMessage, |
| isInjectedHtmlTagForWhichNoProblemsReporting((HtmlTag)tag) ? |
| HighlightInfoType.INFORMATION : |
| SeverityRegistrar.getSeverityRegistrar(tag.getProject()).getHighlightInfoTypeBySeverity(profile.getErrorLevel(key, tag).getSeverity()), |
| addAttributeFix, |
| basicIntention); |
| } else if (!htmlTag) { |
| addElementsForTag( |
| tag, |
| localizedMessage, |
| HighlightInfoType.ERROR, |
| basicIntention |
| ); |
| } |
| } |
| |
| private static boolean isAdditionallyDeclared(final String additional, String name) { |
| name = name.toLowerCase(); |
| if (!additional.contains(name)) return false; |
| |
| StringTokenizer tokenizer = new StringTokenizer(additional, ", "); |
| while (tokenizer.hasMoreTokens()) { |
| if (name.equals(tokenizer.nextToken())) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private static HighlightInfoType getTagProblemInfoType(XmlTag tag) { |
| if (tag instanceof HtmlTag && XmlUtil.HTML_URI.equals(tag.getNamespace())) { |
| if (isInjectedHtmlTagForWhichNoProblemsReporting((HtmlTag)tag)) return HighlightInfoType.INFORMATION; |
| return HighlightInfoType.WARNING; |
| } |
| return HighlightInfoType.WRONG_REF; |
| } |
| |
| private static boolean isInjectedHtmlTagForWhichNoProblemsReporting(HtmlTag tag) { |
| PsiElement context = |
| InjectedLanguageManager.getInstance(tag.getContainingFile().getProject()).getInjectionHost(tag.getContainingFile()); |
| return context != null && skipValidation(context); |
| } |
| |
| public static boolean skipValidation(PsiElement context) { |
| return DO_NOT_VALIDATE.get(context, null); |
| } |
| |
| public static void setSkipValidation(@NotNull PsiElement element) { |
| DO_NOT_VALIDATE.put(element, Boolean.TRUE); |
| } |
| |
| @Override public void visitXmlAttribute(XmlAttribute attribute) {} |
| |
| private void checkAttribute(XmlAttribute attribute) { |
| XmlTag tag = attribute.getParent(); |
| if (tag == null) return; |
| |
| final String name = attribute.getName(); |
| |
| if (XmlExtension.getExtension(attribute.getContainingFile()).needWhitespaceBeforeAttribute()) { |
| PsiElement prevLeaf = PsiTreeUtil.prevLeaf(attribute); |
| |
| if (!(prevLeaf instanceof PsiWhiteSpace)) { |
| TextRange textRange = attribute.getTextRange(); |
| HighlightInfoType type = tag instanceof HtmlTag ? HighlightInfoType.WARNING : HighlightInfoType.ERROR; |
| String description = XmlErrorMessages.message("attribute.should.be.preceded.with.space"); |
| HighlightInfo info = HighlightInfo.newHighlightInfo(type).range(textRange.getStartOffset(), textRange.getStartOffset()).descriptionAndTooltip(description).create(); |
| addToResults(info); |
| } |
| } |
| |
| if (attribute.isNamespaceDeclaration() || XmlUtil.XML_SCHEMA_INSTANCE_URI.equals(attribute.getNamespace())) { |
| //checkReferences(attribute.getValueElement()); |
| return; |
| } |
| |
| XmlElementDescriptor elementDescriptor = tag.getDescriptor(); |
| if (elementDescriptor == null || |
| elementDescriptor instanceof AnyXmlElementDescriptor || |
| ourDoJaxpTesting) { |
| return; |
| } |
| |
| XmlAttributeDescriptor attributeDescriptor = elementDescriptor.getAttributeDescriptor(attribute); |
| |
| if (attributeDescriptor == null) { |
| if (!XmlUtil.attributeFromTemplateFramework(name, tag)) { |
| final String localizedMessage = XmlErrorMessages.message("attribute.is.not.allowed.here", name); |
| final HighlightInfo highlightInfo = reportAttributeProblem(tag, name, attribute, localizedMessage); |
| if (highlightInfo != null) { |
| PsiFile file = tag.getContainingFile(); |
| if (file != null) { |
| for (XmlUndefinedElementFixProvider fixProvider : Extensions.getExtensions(XmlUndefinedElementFixProvider.EP_NAME)) { |
| IntentionAction[] fixes = fixProvider.createFixes(attribute); |
| if (fixes != null) { |
| for (IntentionAction action : fixes) { |
| QuickFixAction.registerQuickFixAction(highlightInfo, action); |
| } |
| break; |
| } |
| } |
| } |
| } |
| } |
| } |
| else { |
| checkDuplicateAttribute(tag, attribute); |
| |
| if (tag instanceof HtmlTag && |
| attribute.getValueElement() == null && |
| !HtmlUtil.isSingleHtmlAttribute(name) |
| ) { |
| final String localizedMessage = XmlErrorMessages.message("empty.attribute.is.not.allowed", name); |
| reportAttributeProblem(tag, name, attribute, localizedMessage); |
| } |
| |
| // we skip resolve of attribute references since there is separate check when taking attribute descriptors |
| PsiReference[] attrRefs = attribute.getReferences(); |
| doCheckRefs(attribute, attrRefs, !attribute.getNamespacePrefix().isEmpty() ? 2 : 1); |
| } |
| } |
| |
| @Nullable |
| private HighlightInfo reportAttributeProblem(final XmlTag tag, |
| final String localName, |
| final XmlAttribute attribute, |
| @NotNull String localizedMessage) { |
| |
| final RemoveAttributeIntentionFix removeAttributeIntention = new RemoveAttributeIntentionFix(localName,attribute); |
| |
| if (!(tag instanceof HtmlTag)) { |
| final HighlightInfoType tagProblemInfoType = HighlightInfoType.WRONG_REF; |
| |
| final ASTNode node = SourceTreeToPsiMap.psiElementToTree(attribute); |
| assert node != null; |
| final ASTNode child = XmlChildRole.ATTRIBUTE_NAME_FINDER.findChild(node); |
| assert child != null; |
| final HighlightInfo highlightInfo = |
| HighlightInfo.newHighlightInfo(tagProblemInfoType).range(child).descriptionAndTooltip(localizedMessage).create(); |
| addToResults(highlightInfo); |
| |
| QuickFixAction.registerQuickFixAction(highlightInfo, removeAttributeIntention); |
| |
| return highlightInfo; |
| } |
| |
| return null; |
| } |
| |
| private void checkDuplicateAttribute(XmlTag tag, final XmlAttribute attribute) { |
| if (skipValidation(tag)) { |
| return; |
| } |
| |
| final XmlAttribute[] attributes = tag.getAttributes(); |
| final PsiFile containingFile = tag.getContainingFile(); |
| final XmlExtension extension = containingFile instanceof XmlFile ? |
| XmlExtension.getExtension(containingFile) : |
| XmlExtension.DEFAULT_EXTENSION; |
| for (XmlAttribute tagAttribute : attributes) { |
| ProgressManager.checkCanceled(); |
| if (attribute != tagAttribute && Comparing.strEqual(attribute.getName(), tagAttribute.getName())) { |
| final String localName = attribute.getLocalName(); |
| |
| if (extension.canBeDuplicated(tagAttribute)) continue; // multiple import attributes are allowed in jsp directive |
| |
| HighlightInfo highlightInfo = HighlightInfo.newHighlightInfo(getTagProblemInfoType(tag)) |
| .range(XmlChildRole.ATTRIBUTE_NAME_FINDER.findChild(SourceTreeToPsiMap.psiElementToTree(attribute))) |
| .descriptionAndTooltip(XmlErrorMessages.message("duplicate.attribute", localName)).create(); |
| addToResults(highlightInfo); |
| |
| IntentionAction intentionAction = new RemoveAttributeIntentionFix(localName, attribute); |
| |
| QuickFixAction.registerQuickFixAction(highlightInfo, intentionAction); |
| } |
| } |
| } |
| |
| @Override public void visitXmlDocument(final XmlDocument document) { |
| if (document.getLanguage() == DTDLanguage.INSTANCE) { |
| final PsiMetaData psiMetaData = document.getMetaData(); |
| if (psiMetaData instanceof Validator) { |
| //noinspection unchecked |
| ((Validator<XmlDocument>)psiMetaData).validate(document, this); |
| } |
| } |
| } |
| |
| @Override public void visitXmlTag(XmlTag tag) { |
| } |
| |
| @Override public void visitXmlAttributeValue(XmlAttributeValue value) { |
| checkReferences(value); |
| |
| final PsiElement parent = value.getParent(); |
| if (!(parent instanceof XmlAttribute)) { |
| return; |
| } |
| |
| XmlAttribute attribute = (XmlAttribute)parent; |
| |
| XmlTag tag = attribute.getParent(); |
| |
| XmlElementDescriptor elementDescriptor = tag.getDescriptor(); |
| XmlAttributeDescriptor attributeDescriptor = elementDescriptor != null ? elementDescriptor.getAttributeDescriptor(attribute):null; |
| |
| if (attributeDescriptor != null && !skipValidation(value)) { |
| String error = attributeDescriptor.validateValue(value, attribute.getValue()); |
| |
| if (error != null) { |
| HighlightInfoType type = getTagProblemInfoType(tag); |
| addToResults(HighlightInfo.newHighlightInfo(type).range(value).descriptionAndTooltip(error).create()); |
| } |
| } |
| } |
| |
| private void checkReferences(PsiElement value) { |
| if (value == null) return; |
| |
| doCheckRefs(value, value.getReferences(), 0); |
| } |
| |
| private void doCheckRefs(final PsiElement value, final PsiReference[] references, int start) { |
| for (int i = start; i < references.length; ++i) { |
| PsiReference reference = references[i]; |
| ProgressManager.checkCanceled(); |
| if (isUrlReference(reference)) continue; |
| if (!hasBadResolve(reference, false)) { |
| continue; |
| } |
| String description = getErrorDescription(reference); |
| |
| final int startOffset = reference.getElement().getTextRange().getStartOffset(); |
| final TextRange referenceRange = reference.getRangeInElement(); |
| |
| // logging for IDEADEV-29655 |
| if (referenceRange.getStartOffset() > referenceRange.getEndOffset()) { |
| LOG.error("Reference range start offset > end offset: " + reference + |
| ", start offset: " + referenceRange.getStartOffset() + ", end offset: " + referenceRange.getEndOffset()); |
| } |
| |
| HighlightInfoType type = getTagProblemInfoType(PsiTreeUtil.getParentOfType(value, XmlTag.class)); |
| if (value instanceof XmlAttributeValue) { |
| PsiElement parent = value.getParent(); |
| if (parent instanceof XmlAttribute) { |
| String name = ((XmlAttribute)parent).getName().toLowerCase(); |
| if (type.getSeverity(null).compareTo(HighlightInfoType.WARNING.getSeverity(null)) > 0 && name.endsWith("stylename")) { |
| type = HighlightInfoType.WARNING; |
| } |
| } |
| } |
| HighlightInfo info = HighlightInfo.newHighlightInfo(type) |
| .range(startOffset + referenceRange.getStartOffset(), startOffset + referenceRange.getEndOffset()) |
| .descriptionAndTooltip(description).create(); |
| addToResults(info); |
| if (reference instanceof LocalQuickFixProvider) { |
| LocalQuickFix[] fixes = ((LocalQuickFixProvider)reference).getQuickFixes(); |
| if (fixes != null) { |
| InspectionManager manager = InspectionManager.getInstance(reference.getElement().getProject()); |
| for (LocalQuickFix fix : fixes) { |
| ProblemDescriptor descriptor = manager.createProblemDescriptor(value, description, fix, |
| ProblemHighlightType.GENERIC_ERROR_OR_WARNING, true); |
| QuickFixAction.registerQuickFixAction(info, new LocalQuickFixAsIntentionAdapter(fix, descriptor)); |
| } |
| } |
| } |
| UnresolvedReferenceQuickFixProvider.registerReferenceFixes(reference, new QuickFixActionRegistrarImpl(info)); |
| } |
| } |
| |
| public static boolean isUrlReference(PsiReference reference) { |
| return reference instanceof FileReferenceOwner || reference instanceof AnchorReference; |
| } |
| |
| @NotNull |
| public static String getErrorDescription(@NotNull PsiReference reference) { |
| String message; |
| if (reference instanceof EmptyResolveMessageProvider) { |
| message = ((EmptyResolveMessageProvider)reference).getUnresolvedMessagePattern(); |
| } |
| else { |
| //noinspection UnresolvedPropertyKey |
| message = PsiBundle.message("cannot.resolve.symbol"); |
| } |
| |
| String description; |
| try { |
| description = BundleBase.format(message, reference.getCanonicalText()); // avoid double formatting |
| } |
| catch (IllegalArgumentException ex) { |
| // unresolvedMessage provided by third-party reference contains wrong format string (e.g. {}), tolerate it |
| description = message; |
| } |
| return description; |
| } |
| |
| public static boolean hasBadResolve(final PsiReference reference, boolean checkSoft) { |
| if (!checkSoft && reference.isSoft()) return false; |
| if (reference instanceof PsiPolyVariantReference) { |
| return ((PsiPolyVariantReference)reference).multiResolve(false).length == 0; |
| } |
| return reference.resolve() == null; |
| } |
| |
| @Override public void visitXmlDoctype(XmlDoctype xmlDoctype) { |
| if (skipValidation(xmlDoctype)) return; |
| checkReferences(xmlDoctype); |
| } |
| |
| private void addToResults(final HighlightInfo info) { |
| myHolder.add(info); |
| } |
| |
| public static void setDoJaxpTesting(boolean doJaxpTesting) { |
| ourDoJaxpTesting = doJaxpTesting; |
| } |
| |
| @Override |
| public void addMessage(PsiElement context, String message, int type) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public void addMessage(PsiElement context, String message, @NotNull ErrorType type) { |
| addMessageWithFixes(context, message, type); |
| } |
| |
| @Override |
| public void addMessageWithFixes(final PsiElement context, final String message, @NotNull final ErrorType type, @NotNull final IntentionAction... fixes) { |
| if (message != null && !message.isEmpty()) { |
| final PsiFile containingFile = context.getContainingFile(); |
| final HighlightInfoType defaultInfoType = type == ErrorType.ERROR ? HighlightInfoType.ERROR : type == ErrorType.WARNING ? HighlightInfoType.WARNING : HighlightInfoType.WEAK_WARNING; |
| |
| if (context instanceof XmlTag && XmlExtension.getExtension(containingFile).shouldBeHighlightedAsTag((XmlTag)context)) { |
| addElementsForTagWithManyQuickFixes((XmlTag)context, message, defaultInfoType, fixes); |
| } |
| else { |
| final PsiElement contextOfFile = InjectedLanguageManager.getInstance(containingFile.getProject()).getInjectionHost(containingFile); |
| final HighlightInfo highlightInfo; |
| |
| if (contextOfFile != null) { |
| TextRange range = InjectedLanguageManager.getInstance(context.getProject()).injectedToHost(context, context.getTextRange()); |
| highlightInfo = HighlightInfo.newHighlightInfo(defaultInfoType).range(range).descriptionAndTooltip(message).create(); |
| } |
| else { |
| highlightInfo = |
| HighlightInfo.newHighlightInfo(HighlightInfoType.WRONG_REF).range(context).descriptionAndTooltip(message).create(); |
| } |
| |
| for (final IntentionAction quickFixAction : fixes) { |
| if (quickFixAction == null) continue; |
| QuickFixAction.registerQuickFixAction(highlightInfo, quickFixAction); |
| } |
| addToResults(highlightInfo); |
| } |
| } |
| } |
| |
| @Override |
| public boolean suitableForFile(@NotNull final PsiFile file) { |
| if (file instanceof XmlFile) return true; |
| |
| for (PsiFile psiFile : file.getViewProvider().getAllFiles()) { |
| if (psiFile instanceof XmlFile) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public void visit(@NotNull final PsiElement element) { |
| element.accept(this); |
| } |
| |
| @Override |
| public boolean analyze(@NotNull final PsiFile file, |
| final boolean updateWholeFile, |
| @NotNull HighlightInfoHolder holder, |
| @NotNull Runnable action) { |
| myHolder = holder; |
| try { |
| action.run(); |
| } |
| finally { |
| myHolder = null; |
| } |
| return true; |
| } |
| |
| @Override |
| @NotNull |
| public HighlightVisitor clone() { |
| return new XmlHighlightVisitor(); |
| } |
| |
| @Override |
| public int order() { |
| return 1; |
| } |
| |
| public static String getUnquotedValue(XmlAttributeValue value, XmlTag tag) { |
| String unquotedValue = value.getValue(); |
| |
| if (tag instanceof HtmlTag) { |
| unquotedValue = unquotedValue.toLowerCase(); |
| } |
| |
| return unquotedValue; |
| } |
| |
| public static boolean shouldBeValidated(@NotNull XmlTag tag) { |
| PsiElement parent = tag.getParent(); |
| if (parent instanceof XmlTag) { |
| return !skipValidation(parent) && !XmlUtil.tagFromTemplateFramework(tag); |
| } |
| return true; |
| } |
| } |