Merge remote-tracking branch 'origin/master' into develar/is
[idea/community.git] / xml / xml-analysis-impl / src / com / intellij / codeInsight / daemon / impl / analysis / XmlHighlightVisitor.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.codeInsight.daemon.impl.analysis;
17
18 import com.intellij.BundleBase;
19 import com.intellij.codeInsight.daemon.*;
20 import com.intellij.codeInsight.daemon.impl.HighlightInfo;
21 import com.intellij.codeInsight.daemon.impl.HighlightInfoType;
22 import com.intellij.codeInsight.daemon.impl.HighlightVisitor;
23 import com.intellij.codeInsight.daemon.impl.SeverityRegistrar;
24 import com.intellij.codeInsight.daemon.impl.quickfix.QuickFixAction;
25 import com.intellij.codeInsight.daemon.impl.quickfix.QuickFixActionRegistrarImpl;
26 import com.intellij.codeInsight.intention.IntentionAction;
27 import com.intellij.codeInsight.quickfix.UnresolvedReferenceQuickFixProvider;
28 import com.intellij.codeInspection.*;
29 import com.intellij.codeInspection.htmlInspections.RequiredAttributesInspectionBase;
30 import com.intellij.codeInspection.htmlInspections.XmlEntitiesInspection;
31 import com.intellij.lang.ASTNode;
32 import com.intellij.lang.dtd.DTDLanguage;
33 import com.intellij.lang.injection.InjectedLanguageManager;
34 import com.intellij.openapi.diagnostic.Logger;
35 import com.intellij.openapi.editor.markup.TextAttributes;
36 import com.intellij.openapi.extensions.Extensions;
37 import com.intellij.openapi.progress.ProgressManager;
38 import com.intellij.openapi.util.Comparing;
39 import com.intellij.openapi.util.TextRange;
40 import com.intellij.openapi.util.UserDataCache;
41 import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
42 import com.intellij.psi.*;
43 import com.intellij.psi.html.HtmlTag;
44 import com.intellij.psi.impl.source.SourceTreeToPsiMap;
45 import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceOwner;
46 import com.intellij.psi.meta.PsiMetaData;
47 import com.intellij.psi.templateLanguages.OuterLanguageElement;
48 import com.intellij.psi.tree.IElementType;
49 import com.intellij.psi.util.PsiTreeUtil;
50 import com.intellij.psi.xml.*;
51 import com.intellij.xml.*;
52 import com.intellij.xml.impl.schema.AnyXmlElementDescriptor;
53 import com.intellij.xml.util.AnchorReference;
54 import com.intellij.xml.util.HtmlUtil;
55 import com.intellij.xml.util.XmlTagUtil;
56 import com.intellij.xml.util.XmlUtil;
57 import org.jetbrains.annotations.NotNull;
58 import org.jetbrains.annotations.Nullable;
59
60 import java.util.HashSet;
61 import java.util.Set;
62 import java.util.StringTokenizer;
63
64 /**
65  * @author Mike
66  */
67 public class XmlHighlightVisitor extends XmlElementVisitor implements HighlightVisitor, IdeValidationHost {
68   private static final Logger LOG = Logger.getInstance("com.intellij.codeInsight.daemon.impl.analysis.XmlHighlightVisitor");
69   private static final UserDataCache<Boolean, PsiElement, Object> DO_NOT_VALIDATE =
70     new UserDataCache<Boolean, PsiElement, Object>("do not validate") {
71     @Override
72     protected Boolean compute(PsiElement parent, Object p) {
73       OuterLanguageElement element = PsiTreeUtil.getChildOfType(parent, OuterLanguageElement.class);
74
75       if (element == null) {
76         // JspOuterLanguageElement is located under XmlText
77         for (PsiElement child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
78           if (child instanceof XmlText) {
79             element = PsiTreeUtil.getChildOfType(child, OuterLanguageElement.class);
80             if (element != null) {
81               break;
82             }
83           }
84         }
85       }
86       if (element == null) return false;
87       PsiFile containingFile = parent.getContainingFile();
88       return containingFile.getViewProvider().getBaseLanguage() != containingFile.getLanguage();
89     }
90   };
91   private static boolean ourDoJaxpTesting;
92
93   private static final TextAttributes NONEMPTY_TEXT_ATTRIBUTES = new TextAttributes() {
94     @Override
95     public boolean isEmpty() {
96       return false;
97     }
98   };
99   private HighlightInfoHolder myHolder;
100
101   public XmlHighlightVisitor() {
102   }
103
104   private void addElementsForTag(XmlTag tag,
105                                  @NotNull String localizedMessage,
106                                  HighlightInfoType type,
107                                  IntentionAction quickFixAction) {
108     addElementsForTagWithManyQuickFixes(tag, localizedMessage, type, quickFixAction);
109   }
110
111   private void addElementsForTagWithManyQuickFixes(XmlTag tag,
112                                                    @NotNull String localizedMessage,
113                                                    HighlightInfoType type, IntentionAction... quickFixActions) {
114     bindMessageToTag(tag, type, -1, localizedMessage, quickFixActions);
115   }
116
117   @Override public void visitXmlToken(XmlToken token) {
118     IElementType tokenType = token.getTokenType();
119     if (tokenType == XmlTokenType.XML_NAME || tokenType == XmlTokenType.XML_TAG_NAME) {
120       PsiElement element = token.getPrevSibling();
121       while(element instanceof PsiWhiteSpace) element = element.getPrevSibling();
122
123       if (element instanceof XmlToken) {
124         if (((XmlToken)element).getTokenType() == XmlTokenType.XML_START_TAG_START) {
125           PsiElement parent = element.getParent();
126
127           if (parent instanceof XmlTag && !(token.getNextSibling() instanceof OuterLanguageElement)) {
128             checkTag((XmlTag)parent);
129           }
130         }
131       } else {
132         PsiElement parent = token.getParent();
133
134         if (parent instanceof XmlAttribute && !(token.getNextSibling() instanceof OuterLanguageElement)) {
135           checkAttribute((XmlAttribute) parent);
136         }
137       }
138     } else if (tokenType == XmlTokenType.XML_DATA_CHARACTERS && token.getParent() instanceof XmlText) {
139       if (token.textContains(']') && token.textContains('>')) {
140
141         String s = token.getText();
142         String marker = "]]>";
143         int i = s.indexOf(marker);
144
145         if (i != -1 ) {                              // TODO: fix
146           XmlTag tag = PsiTreeUtil.getParentOfType(token, XmlTag.class);
147           if (tag != null && XmlExtension.getExtensionByElement(tag).shouldBeHighlightedAsTag(tag) && !skipValidation(tag)) {
148             TextRange textRange = token.getTextRange();
149             int start = textRange.getStartOffset() + i;
150             HighlightInfoType type = tag instanceof HtmlTag ? HighlightInfoType.WARNING : HighlightInfoType.ERROR;
151             String description = XmlErrorMessages.message("cdata.end.should.not.appear.in.content.unless.to.mark.end.of.cdata.section");
152             HighlightInfo info = HighlightInfo.newHighlightInfo(type).range(start, start + marker.length()).descriptionAndTooltip(description).create();
153             addToResults(info);
154           }
155         }
156       }
157     }
158   }
159
160   private void checkTag(XmlTag tag) {
161     if (ourDoJaxpTesting) return;
162
163     if (!myHolder.hasErrorResults()) {
164       checkTagByDescriptor(tag);
165     }
166
167     if (!myHolder.hasErrorResults()) {
168       if (!skipValidation(tag)) {
169         final XmlElementDescriptor descriptor = tag.getDescriptor();
170
171         if (tag instanceof HtmlTag &&
172             ( descriptor instanceof AnyXmlElementDescriptor ||
173               descriptor == null
174             )
175            ) {
176           return;
177         }
178
179         checkReferences(tag);
180       }
181     }
182   }
183
184   private void bindMessageToTag(final XmlTag tag,
185                                 final HighlightInfoType warning,
186                                 final int messageLength,
187                                 @NotNull String localizedMessage,
188                                 IntentionAction... quickFixActions) {
189     XmlToken childByRole = XmlTagUtil.getStartTagNameElement(tag);
190
191     bindMessageToAstNode(childByRole, warning, 0, messageLength, localizedMessage, quickFixActions);
192     childByRole = XmlTagUtil.getEndTagNameElement(tag);
193     bindMessageToAstNode(childByRole, warning, 0, messageLength, localizedMessage, quickFixActions);
194   }
195
196
197   @Override
198   public void visitXmlProcessingInstruction(XmlProcessingInstruction processingInstruction) {
199     super .visitXmlProcessingInstruction(processingInstruction);
200     PsiElement parent = processingInstruction.getParent();
201
202     if (parent instanceof XmlProlog && processingInstruction.getText().startsWith("<?xml")) {
203       for(PsiElement e = PsiTreeUtil.prevLeaf(processingInstruction); e != null; e = PsiTreeUtil.prevLeaf(e)) {
204         if (e instanceof PsiWhiteSpace && PsiTreeUtil.prevLeaf(e) != null ||
205             e instanceof OuterLanguageElement) {
206           continue;
207         }
208         PsiElement eParent = e.getParent();
209         if (eParent instanceof PsiComment) e = eParent;
210         if (eParent instanceof XmlProcessingInstruction) break;
211
212         String description = XmlErrorMessages.message("xml.declaration.should.precede.all.document.content");
213         addToResults(HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(e).descriptionAndTooltip(description).create());
214       }
215     }
216     checkReferences(processingInstruction);
217   }
218
219   private void bindMessageToAstNode(final PsiElement childByRole,
220                                     final HighlightInfoType warning,
221                                     final int offset,
222                                     int length,
223                                     @NotNull String localizedMessage,
224                                     IntentionAction... quickFixActions) {
225     if(childByRole != null) {
226       final TextRange textRange = childByRole.getTextRange();
227       if (length == -1) length = textRange.getLength();
228       final int startOffset = textRange.getStartOffset() + offset;
229
230       HighlightInfo highlightInfo = HighlightInfo.newHighlightInfo(warning).range(childByRole, startOffset, startOffset + length).descriptionAndTooltip(localizedMessage).create();
231
232       if (highlightInfo == null) {
233         highlightInfo = HighlightInfo.newHighlightInfo(warning).range(new TextRange(startOffset, startOffset + length)).textAttributes(NONEMPTY_TEXT_ATTRIBUTES).descriptionAndTooltip(localizedMessage).create();
234       }
235
236       for (final IntentionAction quickFixAction : quickFixActions) {
237         if (quickFixAction == null) continue;
238         QuickFixAction.registerQuickFixAction(highlightInfo, textRange, quickFixAction);
239       }
240       addToResults(highlightInfo);
241     }
242   }
243
244   private void checkTagByDescriptor(final XmlTag tag) {
245     String name = tag.getName();
246
247     XmlElementDescriptor elementDescriptor;
248
249     final PsiElement parent = tag.getParent();
250     if (parent instanceof XmlTag) {
251       XmlTag parentTag = (XmlTag)parent;
252
253       elementDescriptor = XmlUtil.getDescriptorFromContext(tag);
254
255       final XmlElementDescriptor parentDescriptor = parentTag.getDescriptor();
256
257       if (parentDescriptor != null && elementDescriptor == null && shouldBeValidated(tag)) {
258         if (tag instanceof HtmlTag) {
259           //XmlEntitiesInspection inspection = getInspectionProfile(tag, HtmlStyleLocalInspection.SHORT_NAME);
260           //if (inspection != null /*&& isAdditionallyDeclared(inspection.getAdditionalEntries(XmlEntitiesInspection.UNKNOWN_TAG), name)*/) {
261             return;
262           //}
263         }
264
265         addElementsForTag(
266           tag,
267           XmlErrorMessages.message("element.is.not.allowed.here", name),
268           getTagProblemInfoType(tag),
269           null
270         );
271         return;
272       }
273
274       if (elementDescriptor instanceof AnyXmlElementDescriptor ||
275           elementDescriptor == null
276          ) {
277         elementDescriptor = tag.getDescriptor();
278       }
279
280       if (elementDescriptor == null) return;
281     }
282     else {
283       //root tag
284       elementDescriptor = tag.getDescriptor();
285
286      if (elementDescriptor == null) {
287        addElementsForTag(tag, XmlErrorMessages.message("element.must.be.declared", name), HighlightInfoType.WRONG_REF, null);
288        return;
289       }
290     }
291
292     if (!(elementDescriptor instanceof XmlHighlightingAwareElementDescriptor) ||
293         ((XmlHighlightingAwareElementDescriptor)elementDescriptor).shouldCheckRequiredAttributes()) {
294       checkRequiredAttributes(tag, name, elementDescriptor);
295     }
296
297     if (elementDescriptor instanceof Validator) {
298       //noinspection unchecked
299       ((Validator<XmlTag>)elementDescriptor).validate(tag,this);
300     }
301   }
302
303   private void checkRequiredAttributes(XmlTag tag, String name, XmlElementDescriptor elementDescriptor) {
304     XmlAttributeDescriptor[] attributeDescriptors = elementDescriptor.getAttributesDescriptors(tag);
305     Set<String> requiredAttributes = null;
306
307     for (XmlAttributeDescriptor attribute : attributeDescriptors) {
308       if (attribute != null && attribute.isRequired()) {
309         if (requiredAttributes == null) {
310           requiredAttributes = new HashSet<String>();
311         }
312         requiredAttributes.add(attribute.getName(tag));
313       }
314     }
315
316     if (requiredAttributes != null) {
317       for (final String attrName : requiredAttributes) {
318         if (!hasAttribute(tag, attrName) &&
319             !XmlExtension.getExtension(tag.getContainingFile()).isRequiredAttributeImplicitlyPresent(tag, attrName)) {
320
321           IntentionAction insertRequiredAttributeIntention = XmlQuickFixFactory.getInstance().insertRequiredAttributeFix(tag, attrName);
322           final String localizedMessage = XmlErrorMessages.message("element.doesnt.have.required.attribute", name, attrName);
323           final InspectionProfile profile = InspectionProjectProfileManager.getInstance(tag.getProject()).getCurrentProfile();
324           RequiredAttributesInspectionBase inspection =
325             (RequiredAttributesInspectionBase)profile.getUnwrappedTool(XmlEntitiesInspection.REQUIRED_ATTRIBUTES_SHORT_NAME, tag);
326           if (inspection != null) {
327             reportOneTagProblem(
328               tag,
329               attrName,
330               localizedMessage,
331               insertRequiredAttributeIntention,
332               HighlightDisplayKey.find(XmlEntitiesInspection.REQUIRED_ATTRIBUTES_SHORT_NAME),
333               inspection,
334               RequiredAttributesInspectionBase.getIntentionAction(attrName)
335             );
336           }
337         }
338       }
339     }
340   }
341
342   private static boolean hasAttribute(XmlTag tag, String attrName) {
343     final XmlAttribute attribute = tag.getAttribute(attrName);
344     if (attribute == null) return false;
345     if (attribute.getValueElement() != null) return true;
346     if (!(tag instanceof HtmlTag)) return false;
347     final XmlAttributeDescriptor descriptor = attribute.getDescriptor();
348     return descriptor != null && HtmlUtil.isBooleanAttribute(descriptor, tag);
349   }
350
351   private void reportOneTagProblem(final XmlTag tag,
352                                    final String name,
353                                    @NotNull String localizedMessage,
354                                    final IntentionAction basicIntention,
355                                    final HighlightDisplayKey key,
356                                    final XmlEntitiesInspection inspection,
357                                    final IntentionAction addAttributeFix) {
358     boolean htmlTag = false;
359
360     if (tag instanceof HtmlTag) {
361       htmlTag = true;
362       if(isAdditionallyDeclared(inspection.getAdditionalEntries(), name)) return;
363     }
364
365     final InspectionProfile profile = InspectionProjectProfileManager.getInstance(tag.getProject()).getCurrentProfile();
366     if (htmlTag && profile.isToolEnabled(key, tag)) {
367       addElementsForTagWithManyQuickFixes(
368         tag,
369         localizedMessage,
370         isInjectedWithoutValidation(tag) ?
371           HighlightInfoType.INFORMATION :
372           SeverityRegistrar.getSeverityRegistrar(tag.getProject()).getHighlightInfoTypeBySeverity(profile.getErrorLevel(key, tag).getSeverity()),
373         addAttributeFix,
374         basicIntention);
375     } else if (!htmlTag) {
376       addElementsForTag(
377         tag,
378         localizedMessage,
379         HighlightInfoType.ERROR,
380         basicIntention
381       );
382     }
383   }
384
385   private static boolean isAdditionallyDeclared(final String additional, String name) {
386     name = name.toLowerCase();
387     if (!additional.contains(name)) return false;
388
389     StringTokenizer tokenizer = new StringTokenizer(additional, ", ");
390     while (tokenizer.hasMoreTokens()) {
391       if (name.equals(tokenizer.nextToken())) {
392         return true;
393       }
394     }
395
396     return false;
397   }
398
399   private static HighlightInfoType getTagProblemInfoType(XmlTag tag) {
400     if (tag instanceof HtmlTag && XmlUtil.HTML_URI.equals(tag.getNamespace())) {
401       if (isInjectedWithoutValidation(tag)) return HighlightInfoType.INFORMATION;
402       return HighlightInfoType.WARNING;
403     }
404     return HighlightInfoType.WRONG_REF;
405   }
406
407   public static boolean isInjectedWithoutValidation(PsiElement element) {
408     PsiElement context = InjectedLanguageManager.getInstance(element.getProject()).getInjectionHost(element.getContainingFile());
409     return context != null && skipValidation(context);
410   }
411
412   public static boolean skipValidation(PsiElement context) {
413     return DO_NOT_VALIDATE.get(context, null);
414   }
415
416   public static void setSkipValidation(@NotNull PsiElement element) {
417     DO_NOT_VALIDATE.put(element, Boolean.TRUE);
418   }
419
420   @Override public void visitXmlAttribute(XmlAttribute attribute) {}
421
422   private void checkAttribute(XmlAttribute attribute) {
423     XmlTag tag = attribute.getParent();
424     if (tag == null) return;
425
426     final String name = attribute.getName();
427
428     if (XmlExtension.getExtension(attribute.getContainingFile()).needWhitespaceBeforeAttribute()) {
429       PsiElement prevLeaf = PsiTreeUtil.prevLeaf(attribute);
430
431       if (!(prevLeaf instanceof PsiWhiteSpace)) {
432         TextRange textRange = attribute.getTextRange();
433         HighlightInfoType type = tag instanceof HtmlTag ? HighlightInfoType.WARNING : HighlightInfoType.ERROR;
434         String description = XmlErrorMessages.message("attribute.should.be.preceded.with.space");
435         HighlightInfo info = HighlightInfo.newHighlightInfo(type).range(textRange.getStartOffset(), textRange.getStartOffset()).descriptionAndTooltip(description).create();
436         addToResults(info);
437       }
438     }
439
440     if (attribute.isNamespaceDeclaration() || XmlUtil.XML_SCHEMA_INSTANCE_URI.equals(attribute.getNamespace())) {
441       //checkReferences(attribute.getValueElement());
442       return;
443     }
444
445     XmlElementDescriptor elementDescriptor = tag.getDescriptor();
446     if (elementDescriptor == null ||
447         elementDescriptor instanceof AnyXmlElementDescriptor ||
448         ourDoJaxpTesting) {
449       return;
450     }
451
452     XmlAttributeDescriptor attributeDescriptor = elementDescriptor.getAttributeDescriptor(attribute);
453
454     if (attributeDescriptor == null) {
455       if (!XmlUtil.attributeFromTemplateFramework(name, tag)) {
456         final String localizedMessage = XmlErrorMessages.message("attribute.is.not.allowed.here", name);
457         final HighlightInfo highlightInfo = reportAttributeProblem(tag, name, attribute, localizedMessage);
458         if (highlightInfo != null) {
459           PsiFile file = tag.getContainingFile();
460           if (file != null) {
461             for (XmlUndefinedElementFixProvider fixProvider : Extensions.getExtensions(XmlUndefinedElementFixProvider.EP_NAME)) {
462               IntentionAction[] fixes = fixProvider.createFixes(attribute);
463               if (fixes != null) {
464                 for (IntentionAction action : fixes) {
465                   QuickFixAction.registerQuickFixAction(highlightInfo, action);
466                 }
467                 break;
468               }
469             }
470           }
471         }
472       }
473     }
474     else {
475       checkDuplicateAttribute(tag, attribute);
476
477       // we skip resolve of attribute references since there is separate check when taking attribute descriptors
478       PsiReference[] attrRefs = attribute.getReferences();
479       doCheckRefs(attribute, attrRefs, !attribute.getNamespacePrefix().isEmpty() ? 2 : 1);
480     }
481   }
482
483   @Nullable
484   private HighlightInfo reportAttributeProblem(final XmlTag tag,
485                                                final String localName,
486                                                final XmlAttribute attribute,
487                                                @NotNull String localizedMessage) {
488
489     final RemoveAttributeIntentionFix removeAttributeIntention = new RemoveAttributeIntentionFix(localName,attribute);
490
491     if (!(tag instanceof HtmlTag)) {
492       final HighlightInfoType tagProblemInfoType = HighlightInfoType.WRONG_REF;
493
494       final ASTNode node = SourceTreeToPsiMap.psiElementToTree(attribute);
495       assert node != null;
496       final ASTNode child = XmlChildRole.ATTRIBUTE_NAME_FINDER.findChild(node);
497       assert child != null;
498       final HighlightInfo highlightInfo =
499         HighlightInfo.newHighlightInfo(tagProblemInfoType).range(child).descriptionAndTooltip(localizedMessage).create();
500       addToResults(highlightInfo);
501
502       QuickFixAction.registerQuickFixAction(highlightInfo, removeAttributeIntention);
503
504       return highlightInfo;
505     }
506
507     return null;
508   }
509
510   private void checkDuplicateAttribute(XmlTag tag, final XmlAttribute attribute) {
511     if (skipValidation(tag)) {
512       return;
513     }
514
515     final XmlAttribute[] attributes = tag.getAttributes();
516     final PsiFile containingFile = tag.getContainingFile();
517     final XmlExtension extension = containingFile instanceof XmlFile ?
518                                    XmlExtension.getExtension(containingFile) :
519                                    DefaultXmlExtension.DEFAULT_EXTENSION;
520     for (XmlAttribute tagAttribute : attributes) {
521       ProgressManager.checkCanceled();
522       if (attribute != tagAttribute && Comparing.strEqual(attribute.getName(), tagAttribute.getName())) {
523         final String localName = attribute.getLocalName();
524
525         if (extension.canBeDuplicated(tagAttribute)) continue; // multiple import attributes are allowed in jsp directive
526
527         final ASTNode attributeNode = SourceTreeToPsiMap.psiElementToTree(attribute);
528         assert attributeNode != null;
529         final ASTNode attributeNameNode = XmlChildRole.ATTRIBUTE_NAME_FINDER.findChild(attributeNode);
530         assert attributeNameNode != null;
531         HighlightInfo highlightInfo = HighlightInfo.newHighlightInfo(getTagProblemInfoType(tag))
532           .range(attributeNameNode)
533           .descriptionAndTooltip(XmlErrorMessages.message("duplicate.attribute", localName)).create();
534         addToResults(highlightInfo);
535
536         IntentionAction intentionAction = new RemoveAttributeIntentionFix(localName, attribute);
537
538         QuickFixAction.registerQuickFixAction(highlightInfo, intentionAction);
539       }
540     }
541   }
542
543   @Override public void visitXmlDocument(final XmlDocument document) {
544     if (document.getLanguage() == DTDLanguage.INSTANCE) {
545       final PsiMetaData psiMetaData = document.getMetaData();
546       if (psiMetaData instanceof Validator) {
547         //noinspection unchecked
548         ((Validator<XmlDocument>)psiMetaData).validate(document, this);
549       }
550     }
551   }
552
553   @Override public void visitXmlTag(XmlTag tag) {
554   }
555
556   @Override public void visitXmlAttributeValue(XmlAttributeValue value) {
557     checkReferences(value);
558
559     final PsiElement parent = value.getParent();
560     if (!(parent instanceof XmlAttribute)) {
561       return;
562     }
563
564     XmlAttribute attribute = (XmlAttribute)parent;
565
566     XmlTag tag = attribute.getParent();
567
568     XmlElementDescriptor elementDescriptor = tag.getDescriptor();
569     XmlAttributeDescriptor attributeDescriptor = elementDescriptor != null ? elementDescriptor.getAttributeDescriptor(attribute):null;
570
571     if (attributeDescriptor != null && !skipValidation(value)) {
572       String error = attributeDescriptor.validateValue(value, attribute.getValue());
573
574       if (error != null) {
575         HighlightInfoType type = getTagProblemInfoType(tag);
576         addToResults(HighlightInfo.newHighlightInfo(type).range(value).descriptionAndTooltip(error).create());
577       }
578     }
579   }
580
581   private void checkReferences(PsiElement value) {
582     if (value == null) return;
583
584     doCheckRefs(value, value.getReferences(), 0);
585   }
586
587   private void doCheckRefs(final PsiElement value, final PsiReference[] references, int start) {
588     for (int i = start; i < references.length; ++i) {
589       PsiReference reference = references[i];
590       ProgressManager.checkCanceled();
591       if (isUrlReference(reference)) continue;
592       if (!hasBadResolve(reference, false)) {
593         continue;
594       }
595       String description = getErrorDescription(reference);
596
597       final int startOffset = reference.getElement().getTextRange().getStartOffset();
598       final TextRange referenceRange = reference.getRangeInElement();
599
600       // logging for IDEADEV-29655
601       if (referenceRange.getStartOffset() > referenceRange.getEndOffset()) {
602         LOG.error("Reference range start offset > end offset:  " + reference +
603         ", start offset: " + referenceRange.getStartOffset() + ", end offset: " + referenceRange.getEndOffset());
604       }
605
606       HighlightInfoType type = getTagProblemInfoType(PsiTreeUtil.getParentOfType(value, XmlTag.class));
607       if (value instanceof XmlAttributeValue) {
608         PsiElement parent = value.getParent();
609         if (parent instanceof XmlAttribute) {
610           String name = ((XmlAttribute)parent).getName().toLowerCase();
611           if (type.getSeverity(null).compareTo(HighlightInfoType.WARNING.getSeverity(null)) > 0 && name.endsWith("stylename")) {
612             type = HighlightInfoType.WARNING;
613           }
614         }
615       }
616       HighlightInfo info = HighlightInfo.newHighlightInfo(type)
617         .range(startOffset + referenceRange.getStartOffset(), startOffset + referenceRange.getEndOffset())
618         .descriptionAndTooltip(description).create();
619       addToResults(info);
620       if (reference instanceof LocalQuickFixProvider) {
621         LocalQuickFix[] fixes = ((LocalQuickFixProvider)reference).getQuickFixes();
622         if (fixes != null) {
623           InspectionManager manager = InspectionManager.getInstance(reference.getElement().getProject());
624           for (LocalQuickFix fix : fixes) {
625             ProblemDescriptor descriptor = manager.createProblemDescriptor(value, description, fix,
626                                                                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING, true);
627             QuickFixAction.registerQuickFixAction(info, new LocalQuickFixAsIntentionAdapter(fix, descriptor));
628           }
629         }
630       }
631       UnresolvedReferenceQuickFixProvider.registerReferenceFixes(reference, new QuickFixActionRegistrarImpl(info));
632     }
633   }
634
635   public static boolean isUrlReference(PsiReference reference) {
636     return reference instanceof FileReferenceOwner || reference instanceof AnchorReference;
637   }
638
639   @NotNull
640   public static String getErrorDescription(@NotNull PsiReference reference) {
641     String message;
642     if (reference instanceof EmptyResolveMessageProvider) {
643       message = ((EmptyResolveMessageProvider)reference).getUnresolvedMessagePattern();
644     }
645     else {
646       //noinspection UnresolvedPropertyKey
647       message = PsiBundle.message("cannot.resolve.symbol");
648     }
649
650     String description;
651     try {
652       description = BundleBase.format(message, reference.getCanonicalText()); // avoid double formatting
653     }
654     catch (IllegalArgumentException ex) {
655       // unresolvedMessage provided by third-party reference contains wrong format string (e.g. {}), tolerate it
656       description = message;
657     }
658     return description;
659   }
660
661   public static boolean hasBadResolve(final PsiReference reference, boolean checkSoft) {
662     if (!checkSoft && reference.isSoft()) return false;
663     if (reference instanceof PsiPolyVariantReference) {
664       return ((PsiPolyVariantReference)reference).multiResolve(false).length == 0;
665     }
666     return reference.resolve() == null;
667   }
668
669   @Override public void visitXmlDoctype(XmlDoctype xmlDoctype) {
670     if (skipValidation(xmlDoctype)) return;
671     checkReferences(xmlDoctype);
672   }
673
674   private void addToResults(final HighlightInfo info) {
675     myHolder.add(info);
676   }
677
678   public static void setDoJaxpTesting(boolean doJaxpTesting) {
679     ourDoJaxpTesting = doJaxpTesting;
680   }
681
682   @Override
683   public void addMessage(PsiElement context, String message, int type) {
684     throw new UnsupportedOperationException();
685   }
686
687   @Override
688   public void addMessage(PsiElement context, String message, @NotNull ErrorType type) {
689     addMessageWithFixes(context, message, type);
690   }
691
692   @Override
693   public void addMessageWithFixes(final PsiElement context, final String message, @NotNull final ErrorType type, @NotNull final IntentionAction... fixes) {
694     if (message != null && !message.isEmpty()) {
695       final PsiFile containingFile = context.getContainingFile();
696       final HighlightInfoType defaultInfoType = type == ErrorType.ERROR ? HighlightInfoType.ERROR : type == ErrorType.WARNING ? HighlightInfoType.WARNING : HighlightInfoType.WEAK_WARNING;
697
698       if (context instanceof XmlTag && XmlExtension.getExtension(containingFile).shouldBeHighlightedAsTag((XmlTag)context)) {
699         addElementsForTagWithManyQuickFixes((XmlTag)context, message, defaultInfoType, fixes);
700       }
701       else {
702         final PsiElement contextOfFile = InjectedLanguageManager.getInstance(containingFile.getProject()).getInjectionHost(containingFile);
703         final HighlightInfo highlightInfo;
704
705         if (contextOfFile != null) {
706           TextRange range = InjectedLanguageManager.getInstance(context.getProject()).injectedToHost(context, context.getTextRange());
707           highlightInfo = HighlightInfo.newHighlightInfo(defaultInfoType).range(range).descriptionAndTooltip(message).create();
708         }
709         else {
710           highlightInfo =
711             HighlightInfo.newHighlightInfo(HighlightInfoType.WRONG_REF).range(context).descriptionAndTooltip(message).create();
712         }
713
714         for (final IntentionAction quickFixAction : fixes) {
715           if (quickFixAction == null) continue;
716           QuickFixAction.registerQuickFixAction(highlightInfo, quickFixAction);
717         }
718         addToResults(highlightInfo);
719       }
720     }
721   }
722
723   @Override
724   public boolean suitableForFile(@NotNull final PsiFile file) {
725     if (file instanceof XmlFile) return true;
726
727     for (PsiFile psiFile : file.getViewProvider().getAllFiles()) {
728       if (psiFile instanceof XmlFile) {
729         return true;
730       }
731     }
732     return false;
733   }
734
735   @Override
736   public void visit(@NotNull final PsiElement element) {
737     element.accept(this);
738   }
739
740   @Override
741   public boolean analyze(@NotNull final PsiFile file,
742                          final boolean updateWholeFile,
743                          @NotNull HighlightInfoHolder holder,
744                          @NotNull Runnable action) {
745     myHolder = holder;
746     try {
747       action.run();
748     }
749     finally {
750       myHolder = null;
751     }
752     return true;
753   }
754
755   @Override
756   @NotNull
757   public HighlightVisitor clone() {
758     return new XmlHighlightVisitor();
759   }
760
761   @Override
762   public int order() {
763     return 1;
764   }
765
766   public static String getUnquotedValue(XmlAttributeValue value, XmlTag tag) {
767     String unquotedValue = value.getValue();
768
769     if (tag instanceof HtmlTag) {
770       unquotedValue = unquotedValue.toLowerCase();
771     }
772
773     return unquotedValue;
774   }
775
776   public static boolean shouldBeValidated(@NotNull XmlTag tag) {
777     PsiElement parent = tag.getParent();
778     if (parent instanceof XmlTag) {
779       return !skipValidation(parent) && !XmlUtil.tagFromTemplateFramework(tag);
780     }
781     return true;
782   }
783 }