blob: 2880291e769ef9759b1719b1c14339876e033165 [file] [log] [blame]
/*
* Copyright 2005 Sascha Weinreuter
*
* 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 org.intellij.lang.xpath.validation;
import com.intellij.codeInsight.PsiEquivalenceUtil;
import com.intellij.codeInsight.daemon.EmptyResolveMessageProvider;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.lang.ASTNode;
import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import org.intellij.lang.xpath.XPath2TokenTypes;
import org.intellij.lang.xpath.XPathElementType;
import org.intellij.lang.xpath.XPathTokenTypes;
import org.intellij.lang.xpath.context.ContextProvider;
import org.intellij.lang.xpath.context.NamespaceContext;
import org.intellij.lang.xpath.context.VariableContext;
import org.intellij.lang.xpath.context.XPathVersion;
import org.intellij.lang.xpath.context.functions.Function;
import org.intellij.lang.xpath.context.functions.Parameter;
import org.intellij.lang.xpath.psi.*;
import org.intellij.lang.xpath.psi.impl.PrefixedNameImpl;
import org.intellij.lang.xpath.psi.impl.XPathChangeUtil;
import org.intellij.lang.xpath.psi.impl.XPathNumberImpl;
import org.intellij.lang.xpath.xslt.impl.XsltReferenceContributor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.xml.namespace.QName;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public final class XPathAnnotator extends XPath2ElementVisitor implements Annotator {
private AnnotationHolder myHolder;
public void annotate(@NotNull PsiElement psiElement, @NotNull AnnotationHolder holder) {
try {
myHolder = holder;
psiElement.accept(this);
} finally {
myHolder = null;
}
}
@Override
public void visitXPathNodeTest(XPathNodeTest o) {
final ContextProvider contextProvider = o.getXPathContext();
checkNodeTest(contextProvider, myHolder, o);
}
@Override
public void visitXPathStep(XPathStep o) {
checkSillyStep(myHolder, o);
super.visitXPathStep(o);
}
@Override
public void visitXPathNodeTypeTest(XPathNodeTypeTest o) {
checkNodeTypeTest(myHolder, o);
visitXPathExpression(o);
}
@Override
public void visitXPathFunctionCall(XPathFunctionCall o) {
final ContextProvider contextProvider = o.getXPathContext();
checkFunctionCall(myHolder, o, contextProvider);
super.visitXPathFunctionCall(o);
}
@Override
public void visitXPathString(XPathString o) {
checkString(myHolder, o);
super.visitXPathString(o);
}
@Override
public void visitXPathVariableReference(XPathVariableReference o) {
final ContextProvider contextProvider = o.getXPathContext();
checkVariableReference(myHolder, o, contextProvider);
super.visitXPathVariableReference(o);
}
@Override
public void visitXPath2TypeElement(XPath2TypeElement o) {
final ContextProvider contextProvider = o.getXPathContext();
checkPrefixReferences(myHolder, o, contextProvider);
if (o.getDeclaredType() == XPathType.UNKNOWN) {
final PsiReference[] references = o.getReferences();
for (PsiReference reference : references) {
if (reference instanceof XsltReferenceContributor.SchemaTypeReference ) {
if (!reference.isSoft() && reference.resolve() == null) {
final String message = ((EmptyResolveMessageProvider)reference).getUnresolvedMessagePattern();
final Annotation annotation =
myHolder.createErrorAnnotation(reference.getRangeInElement().shiftRight(o.getTextOffset()), message);
annotation.setHighlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
}
}
}
}
super.visitXPath2TypeElement(o);
}
@Override
public void visitXPathNumber(XPathNumber number) {
if (number.getXPathVersion() == XPathVersion.V2) {
final PsiElement leaf = PsiTreeUtil.nextLeaf(number);
if (leaf != null) {
final IElementType elementType = leaf.getNode().getElementType();
if (elementType != XPathTokenTypes.STAR && XPath2TokenTypes.KEYWORDS.contains(elementType)) {
final TextRange range = TextRange.create(number.getTextRange().getStartOffset(), leaf.getTextRange().getEndOffset());
final Annotation annotation =
myHolder.createErrorAnnotation(range, "Number literal must be followed by whitespace in XPath 2");
final XPathBinaryExpression expression = PsiTreeUtil.getParentOfType(number, XPathBinaryExpression.class, true);
if (expression != null) {
final XPathExpression lOperand = expression.getLOperand();
if (number == lOperand) {
final XPathExpression rOperand = expression.getROperand();
if (rOperand != null) {
final String display = number.getText() + " " + expression.getOperationSign();
final String replacement = display + " " + rOperand.getText();
assert PsiEquivalenceUtil.areElementsEquivalent(expression, XPathChangeUtil.createExpression(expression, replacement));
annotation.registerFix(new ExpressionReplacementFix(replacement, display, expression));
}
} else if (number == expression.getROperand()) {
final PsiElement next = PsiTreeUtil.getParentOfType(PsiTreeUtil.nextLeaf(expression), XPathExpression.class, true);
if (next instanceof XPathBinaryExpression) {
final XPathBinaryExpression left = (XPathBinaryExpression)next;
final XPathExpression rOperand = left.getROperand();
if (rOperand != null && lOperand != null) {
final String display = number.getText() + " " + left.getOperationSign();
final String replacement = lOperand.getText() + " " + expression.getOperationSign() + " " + display + " " + rOperand.getText();
assert PsiEquivalenceUtil.areElementsEquivalent(next, XPathChangeUtil.createExpression(next, replacement));
annotation.registerFix(new ExpressionReplacementFix(replacement, display, (XPathExpression)next));
}
}
}
}
}
}
} else {
if (((XPathNumberImpl)number).isScientificNotation()) {
myHolder.createErrorAnnotation(number, "Number literals in scientific notation are not allowed in XPath 1.0");
}
}
super.visitXPathNumber(number);
}
@Override
public void visitXPathBinaryExpression(final XPathBinaryExpression o) {
final XPathExpression operand = o.getLOperand();
if (operand != null && o.getXPathVersion() == XPathVersion.V2) {
final XPathElementType operator = o.getOperator();
if (XPath2TokenTypes.COMP_OPS.contains(operator)) {
if (operand instanceof XPathBinaryExpression && XPath2TokenTypes.COMP_OPS.contains(((XPathBinaryExpression)operand).getOperator())) {
final Annotation annotation = myHolder.createErrorAnnotation(o, "Consecutive comparison is not allowed in XPath 2");
final XPathExpression rOperand = o.getROperand();
if (rOperand != null) {
final String replacement = "(" + operand.getText() + ") " + o.getOperationSign() + " " + rOperand.getText();
annotation.registerFix(new ExpressionReplacementFix(replacement, o));
}
}
if (XPath2TokenTypes.NODE_COMP_OPS.contains(operator)) {
checkApplicability(o, XPath2Type.NODE);
} else if (operator == XPath2TokenTypes.WEQ || operator == XPath2TokenTypes.WNE || operator == XPathTokenTypes.EQ || operator == XPathTokenTypes.NE) {
checkApplicability(o,
XPath2Type.NUMERIC,XPath2Type.BOOLEAN,XPath2Type.STRING,
XPath2Type.DATE,XPath2Type.TIME,XPath2Type.DATETIME,XPath2Type.DURATION,
XPath2Type.HEXBINARY,XPath2Type.BASE64BINARY,XPath2Type.ANYURI,XPath2Type.QNAME);
} else if (operator == XPath2TokenTypes.WGT || operator == XPath2TokenTypes.WGE || operator == XPath2TokenTypes.WLE || operator == XPath2TokenTypes.WLT ||
operator == XPathTokenTypes.GT || operator == XPathTokenTypes.GE || operator == XPathTokenTypes.LE || operator == XPathTokenTypes.LT) {
checkApplicability(o,
XPath2Type.NUMERIC,XPath2Type.BOOLEAN,XPath2Type.STRING,
XPath2Type.DATE,XPath2Type.TIME,XPath2Type.DATETIME,XPath2Type.DURATION,
XPath2Type.ANYURI);
}
} else if (XPath2TokenTypes.UNION_OPS.contains(operator) || XPath2TokenTypes.INTERSECT_EXCEPT.contains(operator)) {
checkApplicability(o, XPath2SequenceType.create(XPath2Type.NODE, XPath2SequenceType.Cardinality.ZERO_OR_MORE));
} else if (operator == XPath2TokenTypes.TO) {
checkApplicability(o, XPath2Type.INTEGER);
} else if (operator == XPathTokenTypes.AND || operator == XPathTokenTypes.OR) {
checkApplicability(o, XPath2Type.BOOLEAN);
} else if (XPathTokenTypes.ADD_OPS.contains(operator)) {
checkApplicability(o, XPath2Type.NUMERIC, XPath2Type.DURATION, XPath2Type.DATE, XPath2Type.TIME, XPath2Type.DATETIME);
} else if (XPath2TokenTypes.MULT_OPS.contains(operator)) {
if (operator == XPath2TokenTypes.IDIV || operator == XPathTokenTypes.MOD) {
checkApplicability(o, XPath2Type.NUMERIC);
} else if (operator == XPathTokenTypes.DIV) {
checkApplicability(o, XPath2Type.NUMERIC, XPath2Type.DURATION);
} else {
assert operator == XPathTokenTypes.MULT;
checkApplicability(o, XPath2Type.NUMERIC, XPath2Type.DURATION);
}
}
}
checkExpression(myHolder, o);
super.visitXPathBinaryExpression(o);
}
private void checkApplicability(XPathBinaryExpression o, XPath2Type... applicableTypes) {
final XPathExpression lOperand = o.getLOperand();
assert lOperand != null;
final XPathType leftType = XPath2Type.mapType(lOperand.getType());
for (XPath2Type applicableType : applicableTypes) {
if (XPathType.isAssignable(applicableType, leftType)) {
return;
}
}
myHolder.createErrorAnnotation(o, "Operator '" + o.getOperationSign() + "' cannot be applied to expressions of type '" + leftType.getName() + "'");
}
@Override
public void visitXPathExpression(XPathExpression o) {
checkExpression(myHolder, o);
}
private static void checkString(AnnotationHolder holder, XPathString string) {
if (!string.isWellFormed()) {
holder.createErrorAnnotation(string, "Malformed string literal");
}
}
private static void checkVariableReference(AnnotationHolder holder, XPathVariableReference reference, @NotNull ContextProvider contextProvider) {
if (reference.resolve() == null) {
final VariableContext variableResolver = contextProvider.getVariableContext();
if (variableResolver == null) return;
if (!variableResolver.canResolve()) {
final Object[] variablesInScope = variableResolver.getVariablesInScope(reference);
if (variablesInScope instanceof String[]) {
final Set<String> variables = new HashSet<String>(Arrays.asList((String[])variablesInScope));
if (!variables.contains(reference.getReferencedName())) {
markUnresolvedVariable(reference, holder);
}
} else if (variablesInScope instanceof QName[]) {
final Set<QName> variables = new HashSet<QName>(Arrays.asList((QName[])variablesInScope));
if (!variables.contains(contextProvider.getQName(reference))) {
markUnresolvedVariable(reference, holder);
}
}
} else {
markUnresolvedVariable(reference, holder);
}
}
}
private static void markUnresolvedVariable(XPathVariableReference reference, AnnotationHolder holder) {
final String referencedName = reference.getReferencedName();
// missing name is already flagged by parser
if (referencedName.length() > 0) {
final TextRange range = reference.getTextRange().shiftRight(1).grown(-1);
final Annotation ann = holder.createErrorAnnotation(range, "Unresolved variable '" + referencedName + "'");
ann.setHighlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
final VariableContext variableContext = ContextProvider.getContextProvider(reference).getVariableContext();
if (variableContext != null) {
final IntentionAction[] fixes = variableContext.getUnresolvedVariableFixes(reference);
for (IntentionAction fix : fixes) {
ann.registerFix(fix);
}
}
}
}
private static void checkSillyStep(AnnotationHolder holder, XPathStep step) {
final XPathExpression previousStep = step.getPreviousStep();
if (previousStep instanceof XPathStep) {
final XPathNodeTest nodeTest = ((XPathStep)previousStep).getNodeTest();
if (nodeTest != null) {
final XPathNodeTest.PrincipalType principalType = nodeTest.getPrincipalType();
if (principalType != XPathNodeTest.PrincipalType.ELEMENT) {
XPathNodeTest test = step.getNodeTest();
if (test != null) {
holder.createWarningAnnotation(test, "Silly location step on " + principalType.getType() + " axis");
}
}
}
}
}
private static void checkFunctionCall(AnnotationHolder holder, XPathFunctionCall call, @NotNull ContextProvider contextProvider) {
final ASTNode node = call.getNode().findChildByType(XPathTokenTypes.FUNCTION_NAME);
if (node == null) {
return;
}
final QName name = contextProvider.getQName(call);
final XPathFunction function = call.resolve();
final Function functionDecl = function != null ? function.getDeclaration() : null;
if (functionDecl == null) {
final PrefixedNameImpl qName = (PrefixedNameImpl)call.getQName();
// need special check for extension functions
if (call.getQName().getPrefix() != null && contextProvider.getFunctionContext().allowsExtensions()) {
final PsiReference[] references = call.getReferences();
if (references.length > 1 && references[1].resolve() == null) {
final Annotation ann = holder.createErrorAnnotation(qName.getPrefixNode(), "Extension namespace prefix '" +
qName.getPrefix() +
"' has not been declared");
ann.setHighlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
} else if (name != null){
final String extNS = name.getNamespaceURI();
if (!StringUtil.isEmpty(extNS)) {
final Set<Pair<QName,Integer>> pairs = contextProvider.getFunctionContext().getFunctions().keySet();
for (Pair<QName, Integer> pair : pairs) {
// extension namespace is known
final String uri = pair.first.getNamespaceURI();
if (uri != null && uri.equals(extNS)) {
holder.createWarningAnnotation(node, "Unknown function '" + name + "'");
}
}
}
}
} else {
if (name != null) {
holder.createWarningAnnotation(node, "Unknown function '" + name + "'");
} else if (qName.getPrefixNode() != null) {
final Annotation ann = holder.createErrorAnnotation(qName.getPrefixNode(), "Extension namespace prefix '" +
qName.getPrefix() +
"' has not been declared");
ann.setHighlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
}
}
} else {
final XPathExpression[] arguments = call.getArgumentList();
for (int i = 0; i < arguments.length; i++) {
checkArgument(holder, arguments[i], i, functionDecl.getParameters());
}
if (arguments.length < functionDecl.getMinArity()) {
if (functionDecl.getMinArity() == 1) {
holder.createErrorAnnotation(node, "Missing argument for function '" + name + "'");
} else {
final Parameter last = functionDecl.getParameters()[functionDecl.getParameters().length - 1];
final String atLeast =
last.kind == Parameter.Kind.OPTIONAL ||
last.kind == Parameter.Kind.VARARG ?
"at least " : "";
holder.createErrorAnnotation(node, "Function '" + name + "' requires " + atLeast + functionDecl.getMinArity() + " arguments");
}
}
}
}
private static void checkArgument(AnnotationHolder holder, XPathExpression argument, int i, Parameter[] parameters) {
if (i >= parameters.length) {
if (parameters.length > 0 && parameters[parameters.length - 1].kind == Parameter.Kind.VARARG) {
// OK. Validate types against the last declared - vararg - param.
} else {
holder.createErrorAnnotation(argument, "Too many arguments");
}
}
}
private static void checkNodeTypeTest(AnnotationHolder holder, XPathNodeTypeTest test) {
final NodeType nodeType = test.getNodeType();
if (nodeType == null) {
return;
}
final XPathExpression[] arguments = test.getArgumentList();
if (test.getXPathVersion() == XPathVersion.V2) {
switch (nodeType) {
case NODE:
case TEXT:
case COMMENT:
markExceedingArguments(holder, arguments, 0);
break;
// TODO: parser doesn't understand TypeName? yet:
// ElementTest ::= "element" "(" (ElementNameOrWildcard ("," TypeName "?"?)?)? ")"
case ELEMENT:
case ATTRIBUTE:
checkKindTestArguments(holder, test, true, 0, 2);
break;
case SCHEMA_ELEMENT:
case SCHEMA_ATTRIBUTE:
checkKindTestArguments(holder, test, false, 1, 1);
break;
case DOCUMENT_NODE:
if (arguments.length >= 1) {
markExceedingArguments(holder, arguments, 1);
final XPathNodeTypeTest argument = findNodeType(arguments[0]);
if (argument != null) {
final NodeType type = argument.getNodeType();
if (type == NodeType.ELEMENT || type == NodeType.SCHEMA_ELEMENT) {
return;
}
}
holder.createErrorAnnotation(arguments[0], "element() or schema-element() expected");
}
break;
case PROCESSING_INSTRUCTION:
if (arguments.length >= 1) {
markExceedingArguments(holder, arguments, 1);
if (arguments[0] instanceof XPathString) {
break;
} else {
final PrefixedName argument = findQName(arguments[0]);
if (argument != null) {
if (argument.getPrefix() == null && !"*".equals(argument.getLocalName())) {
break;
}
}
}
holder.createErrorAnnotation(arguments[0], "String literal or NCName expected");
}
break;
}
} else {
if (arguments.length == 0) {
return;
}
if (test.getNodeType() == NodeType.PROCESSING_INSTRUCTION && arguments.length == 1) {
if (!(arguments[0] instanceof XPathString)) {
holder.createErrorAnnotation(arguments[0], "String literal expected");
}
return;
}
holder.createErrorAnnotation(test, "Invalid number of arguments for node type test '" + nodeType.getType() + "'");
}
}
private static void checkKindTestArguments(AnnotationHolder holder,
XPathNodeTypeTest test,
boolean wildcardAllowed,
int min,
int max) {
final XPathExpression[] arguments = test.getArgumentList();
if (arguments.length >= min) {
for (XPathExpression arg : arguments) {
final PrefixedName argument = findQName(arg);
if (argument == null) {
holder.createErrorAnnotation(arg, "QName expected");
} else {
if (!wildcardAllowed && ("*".equals(argument.getPrefix()) || "*".equals(argument.getLocalName()))) {
holder.createErrorAnnotation(arg, "QName expected");
}
}
}
} else {
holder.createErrorAnnotation(test, "Missing argument for node kind test");
}
markExceedingArguments(holder, arguments, max);
}
private static void markExceedingArguments(AnnotationHolder holder, XPathExpression[] arguments, int start) {
for (int i = start; i < arguments.length; i++) {
holder.createErrorAnnotation(arguments[i], "Too many arguments");
}
}
@Nullable
private static XPathNodeTypeTest findNodeType(XPathExpression argument) {
final XPathNodeTest test = findNodeTest(argument);
if (test != null && !test.isNameTest() && test.getPrincipalType() == XPathNodeTest.PrincipalType.ELEMENT) {
return PsiTreeUtil.getChildOfType(test, XPathNodeTypeTest.class);
}
return null;
}
@Nullable
private static PrefixedName findQName(XPathExpression argument) {
final XPathNodeTest test = findNodeTest(argument);
if (test != null && test.isNameTest() && test.getPrincipalType() == XPathNodeTest.PrincipalType.ELEMENT) {
return test.getQName();
}
return null;
}
@Nullable
private static XPathNodeTest findNodeTest(XPathExpression argument) {
if (argument instanceof XPathLocationPath) {
final XPathStep step = ((XPathLocationPath)argument).getFirstStep();
if (step != null && step.getPreviousStep() == null && step.getPredicates().length == 0) {
final XPathNodeTest test = step.getNodeTest();
if (test != null) {
return test;
}
}
}
return null;
}
private static void checkNodeTest(@NotNull ContextProvider myProvider, AnnotationHolder holder, XPathNodeTest nodeTest) {
checkSillyNodeTest(holder, nodeTest);
checkPrefixReferences(holder, nodeTest, myProvider);
}
private static void checkPrefixReferences(AnnotationHolder holder, QNameElement element, ContextProvider myProvider) {
final PsiReference[] references = element.getReferences();
for (PsiReference reference : references) {
if (reference instanceof PrefixReference) {
final PrefixReference pr = ((PrefixReference)reference);
if (!pr.isSoft() && pr.isUnresolved()) {
final TextRange range = pr.getRangeInElement().shiftRight(pr.getElement().getTextRange().getStartOffset());
final Annotation a = holder.createErrorAnnotation(range, "Unresolved namespace prefix '" + pr.getCanonicalText() + "'");
a.setHighlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
final NamespaceContext namespaceContext = myProvider.getNamespaceContext();
final PrefixedName qName = element.getQName();
if (namespaceContext != null && qName != null) {
final IntentionAction[] fixes = namespaceContext.getUnresolvedNamespaceFixes(reference, qName.getLocalName());
for (IntentionAction fix : fixes) {
a.registerFix(fix);
}
}
}
}
}
}
private static void checkSillyNodeTest(AnnotationHolder holder, XPathNodeTest nodeTest) {
if (nodeTest.getPrincipalType() != XPathNodeTest.PrincipalType.ELEMENT) {
final XPathNodeTypeTest typeTest = PsiTreeUtil.getChildOfType(nodeTest, XPathNodeTypeTest.class);
if (typeTest != null) {
holder.createWarningAnnotation(typeTest, "Silly node type test on axis '" + nodeTest.getPrincipalType().getType() + "'");
}
}
}
@Override
public void visitXPathPredicate(XPathPredicate o) {
final XPathExpression expression = o.getPredicateExpression();
if (expression instanceof XPathLocationPath) {
final XPathExpression parentOfType = PsiTreeUtil.getParentOfType(o, XPathExpression.class, true);
if (parentOfType != null && XPath2Type.ANYATOMICTYPE.isAssignableFrom(parentOfType.getType())) {
myHolder.createErrorAnnotation(expression, "Axis step cannot be used here: the context item is an atomic value");
}
}
super.visitXPathPredicate(o);
}
private static void checkExpression(AnnotationHolder holder, @NotNull XPathExpression expression) {
final XPathType expectedType = ExpectedTypeUtil.getExpectedType(expression);
final XPathType opType = ExpectedTypeUtil.mapType(expression, expression.getType());
if (!XPathType.isAssignable(expectedType, opType)) {
holder.createErrorAnnotation(expression, "Expected type '" + expectedType.getName() + "', got '" + opType.getName() + "'");
}
}
}