blob: a9f91bce7d8ba87919c3a6dd3dcb500e090e4a91 [file] [log] [blame]
/*
* 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.xml.util;
import com.intellij.codeInsight.daemon.impl.analysis.XmlHighlightVisitor;
import com.intellij.lang.Language;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.UserDataCache;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.resolve.reference.impl.providers.IdReferenceProvider;
import com.intellij.psi.impl.source.xml.PossiblePrefixReference;
import com.intellij.psi.impl.source.xml.SchemaPrefix;
import com.intellij.psi.impl.source.xml.SchemaPrefixReference;
import com.intellij.psi.templateLanguages.OuterLanguageElement;
import com.intellij.psi.util.CachedValue;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.util.NullableFunction;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.XmlElementDescriptor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author spleaner
*/
public class XmlRefCountHolder {
private static final Key<CachedValue<XmlRefCountHolder>> xmlRefCountHolderKey = Key.create("xml ref count holder");
private static final UserDataCache<CachedValue<XmlRefCountHolder>, XmlFile, Object> CACHE =
new UserDataCache<CachedValue<XmlRefCountHolder>, XmlFile, Object>() {
@Override
protected CachedValue<XmlRefCountHolder> compute(final XmlFile file, final Object p) {
return CachedValuesManager.getManager(file.getProject()).createCachedValue(new CachedValueProvider<XmlRefCountHolder>() {
@Override
public Result<XmlRefCountHolder> compute() {
final XmlRefCountHolder holder = new XmlRefCountHolder();
final Language language = file.getViewProvider().getBaseLanguage();
final PsiFile psiFile = file.getViewProvider().getPsi(language);
assert psiFile != null;
psiFile.accept(new IdGatheringRecursiveVisitor(holder));
return new Result<XmlRefCountHolder>(holder, file);
}
}, false);
}
};
private final Map<String, List<Pair<XmlAttributeValue, Boolean>>> myId2AttributeListMap = new HashMap<String, List<Pair<XmlAttributeValue, Boolean>>>();
private final Set<XmlAttributeValue> myPossiblyDuplicateIds = new HashSet<XmlAttributeValue>();
private final List<XmlAttributeValue> myIdReferences = new ArrayList<XmlAttributeValue>();
private final Set<String> myAdditionallyDeclaredIds = new HashSet<String>();
private final Set<PsiElement> myDoNotValidateParentsList = new HashSet<PsiElement>();
private final Set<String> myUsedPrefixes = new HashSet<String>();
private final Set<String> myUsedNamespaces = new HashSet<String>();
@Nullable
public static XmlRefCountHolder getRefCountHolder(final XmlElement element) {
PsiFile file = element.getContainingFile();
return file instanceof XmlFile ? CACHE.get(xmlRefCountHolderKey, (XmlFile)file, null).getValue() : null;
}
private XmlRefCountHolder() {
}
public boolean isDuplicateIdAttributeValue(@NotNull final XmlAttributeValue value) {
return myPossiblyDuplicateIds.contains(value);
}
public boolean isValidatable(@Nullable final PsiElement element) {
return !myDoNotValidateParentsList.contains(element);
}
public boolean hasIdDeclaration(@NotNull final String idRef) {
return myId2AttributeListMap.get(idRef) != null || myAdditionallyDeclaredIds.contains(idRef);
}
public boolean isIdReferenceValue(@NotNull final XmlAttributeValue value) {
return myIdReferences.contains(value);
}
private void registerId(@NotNull final String id, @NotNull final XmlAttributeValue attributeValue, final boolean soft) {
List<Pair<XmlAttributeValue, Boolean>> list = myId2AttributeListMap.get(id);
if (list == null) {
list = new ArrayList<Pair<XmlAttributeValue, Boolean>>();
myId2AttributeListMap.put(id, list);
}
else if (!soft) {
final boolean html = HtmlUtil.isHtmlFile(attributeValue);
final boolean html5 = HtmlUtil.isHtml5Context(attributeValue);
// mark as duplicate
List<XmlAttributeValue> notSoft = ContainerUtil.mapNotNull(list, new NullableFunction<Pair<XmlAttributeValue, Boolean>, XmlAttributeValue>() {
@Override
public XmlAttributeValue fun(Pair<XmlAttributeValue, Boolean> pair) {
if (html5 && !"id".equalsIgnoreCase(((XmlAttribute)pair.first.getParent()).getName())) {
// according to HTML 5 (http://www.w3.org/TR/html5/dom.html#the-id-attribute) spec
// only id attribute is unique identifier
return null;
}
if (html && pair.first.getParent().getParent() == attributeValue.getParent().getParent()) {
// according to HTML 4 (http://www.w3.org/TR/html401/struct/global.html#adef-id,
// http://www.w3.org/TR/html401/struct/links.html#h-12.2.3) spec id and name occupy
// same namespace, but having same values on one tag is ok
return null;
}
return pair.second ? null : pair.first;
}
});
if (!notSoft.isEmpty()) {
myPossiblyDuplicateIds.addAll(notSoft);
myPossiblyDuplicateIds.add(attributeValue);
}
}
list.add(new Pair<XmlAttributeValue, Boolean>(attributeValue, soft));
}
private void registerAdditionalId(@NotNull final String id) {
myAdditionallyDeclaredIds.add(id);
}
private void registerIdReference(@NotNull final XmlAttributeValue value) {
myIdReferences.add(value);
}
private void registerOuterLanguageElement(@NotNull final PsiElement element) {
PsiElement parent = element.getParent();
if (parent instanceof XmlText) {
parent = parent.getParent();
}
myDoNotValidateParentsList.add(parent);
}
public boolean isInUse(String prefix) {
return myUsedPrefixes.contains(prefix);
}
public boolean isUsedNamespace(String ns) {
return myUsedNamespaces.contains(ns);
}
private static class IdGatheringRecursiveVisitor extends XmlRecursiveElementVisitor {
private final XmlRefCountHolder myHolder;
private IdGatheringRecursiveVisitor(@NotNull XmlRefCountHolder holder) {
super(true);
myHolder = holder;
}
@Override
public void visitElement(final PsiElement element) {
if (element instanceof OuterLanguageElement) {
visitOuterLanguageElement(element);
}
super.visitElement(element);
}
private void visitOuterLanguageElement(@NotNull final PsiElement element) {
myHolder.registerOuterLanguageElement(element);
PsiReference[] references = element.getReferences();
for (PsiReference reference : references) {
if (reference instanceof PossiblePrefixReference && ((PossiblePrefixReference)reference).isPrefixReference()) {
PsiElement resolve = reference.resolve();
if (resolve instanceof SchemaPrefix) {
myHolder.addUsedPrefix(((SchemaPrefix)resolve).getName());
}
}
}
}
@Override
public void visitComment(final PsiComment comment) {
doVisitAnyComment(comment);
super.visitComment(comment);
}
@Override
public void visitXmlComment(final XmlComment comment) {
doVisitAnyComment(comment);
super.visitXmlComment(comment);
}
private void doVisitAnyComment(final PsiComment comment) {
final String id = XmlDeclareIdInCommentAction.getImplicitlyDeclaredId(comment);
if (id != null) {
myHolder.registerAdditionalId(id);
}
}
@Override
public void visitXmlTag(XmlTag tag) {
myHolder.addUsedPrefix(tag.getNamespacePrefix());
myHolder.addUsedNamespace(tag.getNamespace());
String text = tag.getValue().getTrimmedText();
detectPrefix(text);
super.visitXmlTag(tag);
}
@Override
public void visitXmlAttribute(XmlAttribute attribute) {
if (!attribute.isNamespaceDeclaration()) {
myHolder.addUsedPrefix(attribute.getNamespacePrefix());
}
myHolder.addUsedNamespace(attribute.getNamespace());
super.visitXmlAttribute(attribute);
}
@Override
public void visitXmlAttributeValue(final XmlAttributeValue value) {
final PsiElement element = value.getParent();
if (!(element instanceof XmlAttribute)) return;
final XmlAttribute attribute = (XmlAttribute)element;
final XmlTag tag = attribute.getParent();
if (tag == null) return;
final XmlElementDescriptor descriptor = tag.getDescriptor();
if (descriptor == null) return;
final XmlAttributeDescriptor attributeDescriptor = descriptor.getAttributeDescriptor(attribute);
if (attributeDescriptor != null) {
if (attributeDescriptor.hasIdType()) {
updateMap(attribute, value, false);
}
else {
final PsiReference[] references = value.getReferences();
for (PsiReference r : references) {
if (r instanceof IdReferenceProvider.GlobalAttributeValueSelfReference /*&& !r.isSoft()*/) {
updateMap(attribute, value, r.isSoft());
}
else if (r instanceof SchemaPrefixReference) {
SchemaPrefix prefix = ((SchemaPrefixReference)r).resolve();
if (prefix != null) {
myHolder.addUsedPrefix(prefix.getName());
}
}
}
}
if (attributeDescriptor.hasIdRefType() && PsiTreeUtil.getChildOfType(value, OuterLanguageElement.class) == null) {
myHolder.registerIdReference(value);
}
}
String s = value.getValue();
detectPrefix(s);
super.visitXmlAttributeValue(value);
}
private void detectPrefix(String s) {
if (s != null) {
int pos = s.indexOf(':');
if (pos > 0) {
myHolder.addUsedPrefix(s.substring(0, pos));
}
}
}
private void updateMap(@NotNull final XmlAttribute attribute, @NotNull final XmlAttributeValue value, final boolean soft) {
final String id = XmlHighlightVisitor.getUnquotedValue(value, attribute.getParent());
if (XmlUtil.isSimpleValue(id, value) &&
PsiTreeUtil.getChildOfType(value, OuterLanguageElement.class) == null) {
myHolder.registerId(id, value, soft);
}
}
}
private void addUsedPrefix(String prefix) {
myUsedPrefixes.add(prefix);
}
private void addUsedNamespace(String ns) {
myUsedNamespaces.add(ns);
}
}