diff --git a/plugins/xpath/xpath-lang/src/org/intellij/lang/xpath/psi/impl/XPathElementImpl.java b/plugins/xpath/xpath-lang/src/org/intellij/lang/xpath/psi/impl/XPathElementImpl.java index 8191b98dde5e..cce97397dfdc 100644 --- a/plugins/xpath/xpath-lang/src/org/intellij/lang/xpath/psi/impl/XPathElementImpl.java +++ b/plugins/xpath/xpath-lang/src/org/intellij/lang/xpath/psi/impl/XPathElementImpl.java @@ -17,10 +17,12 @@ package org.intellij.lang.xpath.psi.impl; import com.intellij.extapi.psi.ASTWrapperPsiElement; import com.intellij.lang.ASTNode; +import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.openapi.util.NotNullLazyValue; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.impl.PsiTreeDebugBuilder; +import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; import com.intellij.util.IncorrectOperationException; import org.intellij.lang.xpath.XPath2ElementTypes; import org.intellij.lang.xpath.XPathElementTypes; @@ -34,100 +36,100 @@ import org.jetbrains.annotations.NotNull; public class XPathElementImpl extends ASTWrapperPsiElement implements XPathElement { - private final NotNullLazyValue myContext = new NotNullLazyValue() { - @NotNull - @Override - protected synchronized ContextProvider compute() { - return ContextProvider.getContextProvider(XPathElementImpl.this); - } - }; - - public XPathElementImpl(ASTNode node) { - super(node); - } - - public String toString() { - final String name = getClass().getName(); - return name.substring(name.lastIndexOf('.') + 1) + ": " + getText(); - } - - public PsiElement addBefore(@NotNull PsiElement psiElement, final PsiElement anchor) throws IncorrectOperationException { - final ASTNode node = getNode(); - final ASTNode child = psiElement.getNode(); - assert child != null; - node.addChild(child, anchor.getNode()); - return node.getPsi(); - } - - public PsiElement addAfter(@NotNull PsiElement psiElement, final PsiElement anchor) throws IncorrectOperationException { - final ASTNode astNode = anchor.getNode(); - assert astNode != null; - final ASTNode next = astNode.getTreeNext(); - - final ASTNode node = getNode(); - final ASTNode newNode = psiElement.getNode(); - assert newNode != null; - if (next != null) { - node.addChild(newNode, next); - } else { - node.addChild(newNode); - } - return node.getPsi(); - } - - public PsiElement add(@NotNull PsiElement psiElement) throws IncorrectOperationException { - final ASTNode child = psiElement.getNode(); - assert child != null; - getNode().addChild(child); - return getNode().getPsi(); - } - - public void delete() throws IncorrectOperationException { - final ASTNode node = getNode(); - - final ASTNode parent = node.getTreeParent(); - final ASTNode next = node.getTreeNext(); - parent.removeChild(node); - - if (XPath2ElementTypes.EXPRESSIONS.contains(node.getElementType())) { - if (parent.getElementType() == XPathElementTypes.FUNCTION_CALL) { - if (next != null && next.getElementType() == XPathTokenTypes.COMMA) { - parent.removeChild(next); - } - } - } - } - - public PsiElement replace(@NotNull PsiElement psiElement) throws IncorrectOperationException { - final ASTNode newNode = psiElement.getNode(); - final ASTNode myNode = getNode(); - - assert newNode != null; - myNode.getTreeParent().replaceChild(myNode, newNode); - - return newNode.getPsi(); - } - + private final NotNullLazyValue myContext = new NotNullLazyValue() { @NotNull - @SuppressWarnings({ "ConstantConditions", "EmptyMethod" }) - public final ASTNode getNode() { - return super.getNode(); - } - @Override - public XPathFile getContainingFile() { - return (XPathFile)super.getContainingFile(); + protected synchronized ContextProvider compute() { + return ContextProvider.getContextProvider(XPathElementImpl.this); } + }; - @Override - public ContextProvider getXPathContext() { - return myContext.getValue(); - } + public XPathElementImpl(ASTNode node) { + super(node); + } - @Override - public XPathVersion getXPathVersion() { - return getContainingFile().getXPathVersion(); + public String toString() { + final String name = getClass().getName(); + return name.substring(name.lastIndexOf('.') + 1) + ": " + getText(); + } + + public PsiElement addBefore(@NotNull PsiElement psiElement, final PsiElement anchor) throws IncorrectOperationException { + final ASTNode node = getNode(); + final ASTNode child = psiElement.getNode(); + assert child != null; + node.addChild(child, anchor.getNode()); + return node.getPsi(); + } + + public PsiElement addAfter(@NotNull PsiElement psiElement, final PsiElement anchor) throws IncorrectOperationException { + final ASTNode astNode = anchor.getNode(); + assert astNode != null; + final ASTNode next = astNode.getTreeNext(); + + final ASTNode node = getNode(); + final ASTNode newNode = psiElement.getNode(); + assert newNode != null; + if (next != null) { + node.addChild(newNode, next); + } else { + node.addChild(newNode); } + return node.getPsi(); + } + + public PsiElement add(@NotNull PsiElement psiElement) throws IncorrectOperationException { + final ASTNode child = psiElement.getNode(); + assert child != null; + getNode().addChild(child); + return getNode().getPsi(); + } + + public void delete() throws IncorrectOperationException { + final ASTNode node = getNode(); + + final ASTNode parent = node.getTreeParent(); + final ASTNode next = node.getTreeNext(); + parent.removeChild(node); + + if (XPath2ElementTypes.EXPRESSIONS.contains(node.getElementType())) { + if (parent.getElementType() == XPathElementTypes.FUNCTION_CALL) { + if (next != null && next.getElementType() == XPathTokenTypes.COMMA) { + parent.removeChild(next); + } + } + } + } + + public PsiElement replace(@NotNull PsiElement psiElement) throws IncorrectOperationException { + final ASTNode newNode = psiElement.getNode(); + final ASTNode myNode = getNode(); + + assert newNode != null; + myNode.getTreeParent().replaceChild(myNode, newNode); + + return newNode.getPsi(); + } + + @NotNull + @SuppressWarnings({ "ConstantConditions", "EmptyMethod" }) + public final ASTNode getNode() { + return super.getNode(); + } + + @Override + public XPathFile getContainingFile() { + return (XPathFile)super.getContainingFile(); + } + + @Override + public ContextProvider getXPathContext() { + return myContext.getValue(); + } + + @Override + public XPathVersion getXPathVersion() { + return getContainingFile().getXPathVersion(); + } protected String unexpectedPsiAssertion() { final PsiTreeDebugBuilder builder = new PsiTreeDebugBuilder(); @@ -144,6 +146,14 @@ public class XPathElementImpl extends ASTWrapperPsiElement implements XPathEleme } public void accept(XPathElementVisitor visitor) { - visitor.visitXPathElement(this); + visitor.visitXPathElement(this); + } + + public final String getUnescapedText() { + if (InjectedLanguageUtil.isInInjectedLanguagePrefixSuffix(this)) { + // do not attempt to decode text if PsiElement is part of prefix/suffix + return getText(); } + return InjectedLanguageManager.getInstance(getProject()).getUnescapedText(this); + } } diff --git a/plugins/xpath/xpath-lang/src/org/intellij/lang/xpath/psi/impl/XPathStringImpl.java b/plugins/xpath/xpath-lang/src/org/intellij/lang/xpath/psi/impl/XPathStringImpl.java index 59a7fab8fbff..ec3eb3ce16ea 100644 --- a/plugins/xpath/xpath-lang/src/org/intellij/lang/xpath/psi/impl/XPathStringImpl.java +++ b/plugins/xpath/xpath-lang/src/org/intellij/lang/xpath/psi/impl/XPathStringImpl.java @@ -16,8 +16,7 @@ package org.intellij.lang.xpath.psi.impl; import com.intellij.lang.ASTNode; -import com.intellij.lang.Language; -import org.intellij.lang.xpath.XPath2Language; +import org.intellij.lang.xpath.context.XPathVersion; import org.intellij.lang.xpath.psi.XPathElementVisitor; import org.intellij.lang.xpath.psi.XPathString; import org.intellij.lang.xpath.psi.XPathType; @@ -34,34 +33,54 @@ public class XPathStringImpl extends XPathElementImpl implements XPathString { } public boolean isWellFormed() { - final boolean xpath2 = getContainingFile().getLanguage() == Language.findInstance(XPath2Language.class); - final String text = getText(); - if (text.startsWith("'")) { - if (!text.endsWith("'")) { - return false; - } - if (!xpath2 && getValue().indexOf('\'') != getValue().lastIndexOf('\'')) { - return false; - } - } else if (text.startsWith("\"")) { - if (!text.endsWith("\"")) { - return false; - } - if (!xpath2 && getValue().indexOf('\"') != getValue().lastIndexOf('"')) { + final String text = getUnescapedText(); + final char quoteChar = getQuoteChar(); + if (!text.endsWith(String.valueOf(quoteChar))) { + return false; + } + + if (getXPathVersion() == XPathVersion.V2) { + final String value = getStringBetweenQuotes(); + final String unescaped = unescape(quoteChar, value); + return escape(quoteChar, unescaped).equals(value); + } else { + if (getValue().indexOf(quoteChar) != getValue().lastIndexOf(quoteChar)) { return false; } } - return xpath2 || text.indexOf('\n') == -1 && text.indexOf("\r") == -1; + + return text.indexOf('\n') == -1 && text.indexOf('\r') == -1; } public String getValue() { - final String value = getText().substring(0, getTextLength() - 1); - if (getContainingFile().getLanguage() == Language.findInstance(XPath2Language.class)) { - return value.replaceAll("\"\"", "\"").replaceAll("''", "'"); + final String value = getStringBetweenQuotes(); + if (getXPathVersion() == XPathVersion.V2) { + return unescape(getQuoteChar(), value); } return value; } + private String getStringBetweenQuotes() { + final String text = getUnescapedText(); + return text.endsWith(String.valueOf(getQuoteChar())) ? text.substring(1, text.length() - 1) : text.substring(1, text.length()); + } + + private char getQuoteChar() { + return getUnescapedText().charAt(0); + } + + private static String unescape(char quote, String value) { + final String singleQuote = String.valueOf(quote); + final String escapedQuote = singleQuote + quote; + return value.replaceAll(escapedQuote, singleQuote); + } + + private static String escape(char quote, String value) { + final String singleQuote = String.valueOf(quote); + final String escapedQuote = singleQuote + quote; + return value.replaceAll(singleQuote, escapedQuote); + } + public void accept(XPathElementVisitor visitor) { visitor.visitXPathString(this); } diff --git a/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/XPath2HighlightingTest.java b/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/XPath2HighlightingTest.java index 4702751d5a01..08b58dd1d83f 100644 --- a/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/XPath2HighlightingTest.java +++ b/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/XPath2HighlightingTest.java @@ -37,6 +37,18 @@ public class XPath2HighlightingTest extends XPath2HighlightingTestBase { doXPathHighlighting(); } + public void testMalformedStringLiteral() throws Throwable { + doXPathHighlighting(); + } + + public void testQuotedStringLiteral() throws Throwable { + doXPathHighlighting(); + } + + public void testEmptyStringLiteral() throws Throwable { + doXPathHighlighting(); + } + @Override protected String getSubPath() { return "xpath/highlighting"; diff --git a/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/XPathHighlightingTest.java b/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/XPathHighlightingTest.java index 4069b9172733..8d0190d57a84 100644 --- a/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/XPathHighlightingTest.java +++ b/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/XPathHighlightingTest.java @@ -64,6 +64,14 @@ public class XPathHighlightingTest extends TestBase { doXPathHighlighting(); } + public void testMalformedStringLiteral() throws Throwable { + doXPathHighlighting(); + } + + public void testQuotedStringLiteral() throws Throwable { + doXPathHighlighting(); + } + private void doXPathHighlighting(String... moreFiles) throws Throwable { final String name = getTestFileName(); myFixture.testHighlighting(true, false, false, ArrayUtil.append(moreFiles, name + ".xpath")); diff --git a/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/xslt/Xslt2HighlightingTest.java b/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/xslt/Xslt2HighlightingTest.java index 90e01a028acb..9e275609906c 100644 --- a/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/xslt/Xslt2HighlightingTest.java +++ b/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/xslt/Xslt2HighlightingTest.java @@ -49,6 +49,10 @@ public class Xslt2HighlightingTest extends TestBase { doXsltHighlighting(); } + public void testEscapedXPathString() throws Throwable { + doXsltHighlighting(); + } + private void doXsltHighlighting(String... moreFiles) throws Throwable { final String name = getTestFileName(); myFixture.testHighlighting(true, false, false, ArrayUtil.append(moreFiles, name + ".xsl")); diff --git a/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/xslt/XsltHighlightingTest.java b/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/xslt/XsltHighlightingTest.java index 2db116f6d1be..2ac056f672ee 100644 --- a/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/xslt/XsltHighlightingTest.java +++ b/plugins/xpath/xpath-lang/test/org/intellij/lang/xpath/xslt/XsltHighlightingTest.java @@ -115,6 +115,10 @@ public class XsltHighlightingTest extends TestBase { doXsltHighlighting(); } + public void testEscapedXPathString() throws Throwable { + doXsltHighlighting(); + } + public void testTemplateWithPrefix() throws Throwable { myFixture.enableInspections(XmlUnusedNamespaceInspection.class); doXsltHighlighting(); diff --git a/plugins/xpath/xpath-lang/testData/xpath/highlighting/emptyStringLiteral.xpath2 b/plugins/xpath/xpath-lang/testData/xpath/highlighting/emptyStringLiteral.xpath2 new file mode 100644 index 000000000000..0a06903a0e51 --- /dev/null +++ b/plugins/xpath/xpath-lang/testData/xpath/highlighting/emptyStringLiteral.xpath2 @@ -0,0 +1 @@ +concat("", '') \ No newline at end of file diff --git a/plugins/xpath/xpath-lang/testData/xpath/highlighting/malformedStringLiteral.xpath b/plugins/xpath/xpath-lang/testData/xpath/highlighting/malformedStringLiteral.xpath new file mode 100644 index 000000000000..993be6453d47 --- /dev/null +++ b/plugins/xpath/xpath-lang/testData/xpath/highlighting/malformedStringLiteral.xpath @@ -0,0 +1 @@ +'abc \ No newline at end of file diff --git a/plugins/xpath/xpath-lang/testData/xpath/highlighting/malformedStringLiteral.xpath2 b/plugins/xpath/xpath-lang/testData/xpath/highlighting/malformedStringLiteral.xpath2 new file mode 100644 index 000000000000..f1b27a94e072 --- /dev/null +++ b/plugins/xpath/xpath-lang/testData/xpath/highlighting/malformedStringLiteral.xpath2 @@ -0,0 +1 @@ +'abc'' \ No newline at end of file diff --git a/plugins/xpath/xpath-lang/testData/xpath/highlighting/quotedStringLiteral.xpath b/plugins/xpath/xpath-lang/testData/xpath/highlighting/quotedStringLiteral.xpath new file mode 100644 index 000000000000..c23ec45acb28 --- /dev/null +++ b/plugins/xpath/xpath-lang/testData/xpath/highlighting/quotedStringLiteral.xpath @@ -0,0 +1,5 @@ +concat( +'a''b', +"c""d", +"""xz""", +'''') \ No newline at end of file diff --git a/plugins/xpath/xpath-lang/testData/xpath/highlighting/quotedStringLiteral.xpath2 b/plugins/xpath/xpath-lang/testData/xpath/highlighting/quotedStringLiteral.xpath2 new file mode 100644 index 000000000000..7b48f2994a12 --- /dev/null +++ b/plugins/xpath/xpath-lang/testData/xpath/highlighting/quotedStringLiteral.xpath2 @@ -0,0 +1 @@ +concat('a''b', "c""d", """xz""", '''') \ No newline at end of file diff --git a/plugins/xpath/xpath-lang/testData/xslt/highlighting/escapedXPathString.xsl b/plugins/xpath/xpath-lang/testData/xslt/highlighting/escapedXPathString.xsl new file mode 100644 index 000000000000..43e6b4973c7c --- /dev/null +++ b/plugins/xpath/xpath-lang/testData/xslt/highlighting/escapedXPathString.xsl @@ -0,0 +1,8 @@ + + + + + + 'a''b'" /> + + \ No newline at end of file diff --git a/plugins/xpath/xpath-lang/testData/xslt2/highlighting/escapedXPathString.xsl b/plugins/xpath/xpath-lang/testData/xslt2/highlighting/escapedXPathString.xsl new file mode 100644 index 000000000000..9eae3729254f --- /dev/null +++ b/plugins/xpath/xpath-lang/testData/xslt2/highlighting/escapedXPathString.xsl @@ -0,0 +1,9 @@ + + + + + + + 'a''" /> + + \ No newline at end of file