From 26ddddac2b6e3d9a40a401d9202cc7bf7b74e22f Mon Sep 17 00:00:00 2001 From: Bas Leijdekkers Date: Fri, 18 May 2018 16:58:20 +0200 Subject: [PATCH] RegExp: replace in injected context fixes (IDEA-186023, IDEA-191355) document based replacement avoids problems when using the Edit Injected Fragment intention context manipulator handles escaping in injected context --- .../inspection/OctalEscapeInspection.java | 27 +------- .../inspection/RedundantEscapeInspection.java | 29 ++------ .../inspection/RegExpReplacementUtil.java | 66 ++++++++----------- .../SingleCharAlternationInspection.java | 29 ++------ .../regexp/psi/impl/RegExpElementImpl.java | 12 ++-- .../SingleCharAlternationInspectionTest.java | 20 +----- 6 files changed, 48 insertions(+), 135 deletions(-) diff --git a/RegExpSupport/src/org/intellij/lang/regexp/inspection/OctalEscapeInspection.java b/RegExpSupport/src/org/intellij/lang/regexp/inspection/OctalEscapeInspection.java index d23e7ce28be0..2cc0ded304ab 100644 --- a/RegExpSupport/src/org/intellij/lang/regexp/inspection/OctalEscapeInspection.java +++ b/RegExpSupport/src/org/intellij/lang/regexp/inspection/OctalEscapeInspection.java @@ -1,29 +1,13 @@ -/* - * Copyright 2000-2017 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.intellij.lang.regexp.inspection; import com.intellij.codeInspection.LocalInspectionTool; import com.intellij.codeInspection.LocalQuickFix; import com.intellij.codeInspection.ProblemDescriptor; import com.intellij.codeInspection.ProblemsHolder; -import com.intellij.lang.ASTNode; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; -import org.intellij.lang.regexp.RegExpTT; import org.intellij.lang.regexp.psi.RegExpChar; import org.intellij.lang.regexp.psi.RegExpElementVisitor; import org.jetbrains.annotations.Nls; @@ -67,8 +51,7 @@ public class OctalEscapeInspection extends LocalInspectionTool { static String buildReplacementText(RegExpChar aChar) { final int value = aChar.getValue(); final String hex = Integer.toHexString(value); - final String result = (hex.length() == 1 ? "\\x0" : "\\x") + hex; - return RegExpReplacementUtil.escapeForContext(result, aChar); + return (hex.length() == 1 ? "\\x0" : "\\x") + hex; } private static class ReplaceWithHexEscapeFix implements LocalQuickFix { @@ -98,11 +81,7 @@ public class OctalEscapeInspection extends LocalInspectionTool { if (!(element instanceof RegExpChar)) { return; } - final ASTNode node = element.getNode(); - final RegExpChar aChar = (RegExpChar)element; - final ASTNode anchor = node.getFirstChildNode(); - node.addLeaf(RegExpTT.HEX_CHAR, buildReplacementText(aChar), anchor); - node.removeChild(anchor); + RegExpReplacementUtil.replaceInContext(element, buildReplacementText((RegExpChar)element)); } } } \ No newline at end of file diff --git a/RegExpSupport/src/org/intellij/lang/regexp/inspection/RedundantEscapeInspection.java b/RegExpSupport/src/org/intellij/lang/regexp/inspection/RedundantEscapeInspection.java index d2de5a8baa0a..632a8fc5ce62 100644 --- a/RegExpSupport/src/org/intellij/lang/regexp/inspection/RedundantEscapeInspection.java +++ b/RegExpSupport/src/org/intellij/lang/regexp/inspection/RedundantEscapeInspection.java @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2017 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.intellij.lang.regexp.inspection; import com.intellij.codeInspection.LocalInspectionTool; @@ -85,20 +71,15 @@ public class RedundantEscapeInspection extends LocalInspectionTool { if (!(element instanceof RegExpChar)) { return; } - final RegExpChar aChar = (RegExpChar)element; - final ASTNode node = aChar.getNode().getFirstChildNode(); - final ASTNode parent = node.getTreeParent(); - parent.addLeaf(RegExpTT.CHARACTER, replacement(aChar), node); - parent.removeChild(node); + RegExpReplacementUtil.replaceInContext(element, replacement((RegExpChar)element)); } @NotNull private static String replacement(RegExpChar aChar) { final int codePoint = aChar.getValue(); - final String s = Character.isSupplementaryCodePoint(codePoint) - ? Character.toString(Character.highSurrogate(codePoint)) + Character.toString(Character.lowSurrogate(codePoint)) - : Character.toString((char)codePoint); - return RegExpReplacementUtil.escapeForContext(s, aChar); + return Character.isSupplementaryCodePoint(codePoint) + ? Character.toString(Character.highSurrogate(codePoint)) + Character.toString(Character.lowSurrogate(codePoint)) + : Character.toString((char)codePoint); } } } diff --git a/RegExpSupport/src/org/intellij/lang/regexp/inspection/RegExpReplacementUtil.java b/RegExpSupport/src/org/intellij/lang/regexp/inspection/RegExpReplacementUtil.java index 590a1760b735..2c4e3ebbcb98 100644 --- a/RegExpSupport/src/org/intellij/lang/regexp/inspection/RegExpReplacementUtil.java +++ b/RegExpSupport/src/org/intellij/lang/regexp/inspection/RegExpReplacementUtil.java @@ -1,29 +1,16 @@ -/* - * Copyright 2000-2017 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.intellij.lang.regexp.inspection; +import com.intellij.lang.injection.InjectedLanguageManager; +import com.intellij.openapi.editor.Document; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.psi.ElementManipulator; -import com.intellij.psi.ElementManipulators; -import com.intellij.psi.PsiElement; +import com.intellij.psi.*; import com.intellij.psi.xml.XmlElement; import com.intellij.xml.util.XmlStringUtil; import org.intellij.lang.regexp.psi.RegExpElement; import org.intellij.lang.regexp.psi.impl.RegExpElementImpl; +import org.jetbrains.annotations.NotNull; /** * @author Bas Leijdekkers @@ -32,28 +19,29 @@ public class RegExpReplacementUtil { private RegExpReplacementUtil() {} - public static String escapeForContext(String text, RegExpElement element) { - final PsiElement context = element.getContainingFile().getContext(); - ElementManipulator manipulator = context == null ? null : ElementManipulators.getManipulator(context); - if (manipulator != null) { - // use element manipulator to process escape sequences correctly for all supported languages - PsiElement copy = context.copy(); // create a copy to avoid original element modifications - PsiElement newElement = manipulator.handleContentChange(copy, text); - if (newElement != null) { - String newElementText = newElement.getText(); - TextRange newRange = manipulator.getRangeInElement(newElement); - return newElementText.substring(newRange.getStartOffset(), newRange.getEndOffset()); + public static void replaceInContext(@NotNull PsiElement element, @NotNull String text) { + final PsiFile file = element.getContainingFile(); + final InjectedLanguageManager injectedLanguageManager = InjectedLanguageManager.getInstance(element.getProject()); + if (injectedLanguageManager.isInjectedFragment(file)) { + final PsiElement context = file.getContext(); + ElementManipulator manipulator = context == null ? null : ElementManipulators.getManipulator(context); + if (manipulator != null) { + // use element manipulator to process escape sequences correctly for all supported languages + final TextRange range = manipulator.getRangeInElement(context); + if (manipulator.handleContentChange(context, range.cutOut(element.getTextRange()), text) != null) { + return; + } + } + if (RegExpElementImpl.isLiteralExpression(context)) { + text = StringUtil.escapeStringCharacters(text); + } + else if (context instanceof XmlElement) { + text = XmlStringUtil.escapeString(text); } } - if (RegExpElementImpl.isLiteralExpression(context)) { - // otherwise, just pretend it is a Java-style string - return StringUtil.escapeStringCharacters(text); - } - else if (context instanceof XmlElement) { - return XmlStringUtil.escapeString(text); - } - else { - return text; - } + final Document document = PsiDocumentManager.getInstance(element.getProject()).getDocument(file); + assert document != null; + final TextRange replaceRange = element.getTextRange(); + document.replaceString(replaceRange.getStartOffset(), replaceRange.getEndOffset(), text); } } diff --git a/RegExpSupport/src/org/intellij/lang/regexp/inspection/SingleCharAlternationInspection.java b/RegExpSupport/src/org/intellij/lang/regexp/inspection/SingleCharAlternationInspection.java index b421eefe3e17..ec622931fde9 100644 --- a/RegExpSupport/src/org/intellij/lang/regexp/inspection/SingleCharAlternationInspection.java +++ b/RegExpSupport/src/org/intellij/lang/regexp/inspection/SingleCharAlternationInspection.java @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2017 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.intellij.lang.regexp.inspection; import com.intellij.codeInspection.LocalInspectionTool; @@ -104,15 +90,10 @@ public class SingleCharAlternationInspection extends LocalInspectionTool { return; } final RegExpPattern pattern = (RegExpPattern)element; - final String text = buildReplacementText(pattern); - final RegExpBranch branch = RegExpFactory.createBranchFromText(text, element); final PsiElement parent = pattern.getParent(); - if (parent instanceof RegExpGroup && ((RegExpGroup)parent).getType() == RegExpGroup.Type.NON_CAPTURING) { - parent.replace(branch.getAtoms()[0]); - } - else { - pattern.replace(branch.getAtoms()[0]); - } + final PsiElement victim = + (parent instanceof RegExpGroup && ((RegExpGroup)parent).getType() == RegExpGroup.Type.NON_CAPTURING) ? parent : pattern; + RegExpReplacementUtil.replaceInContext(victim, buildReplacementText(pattern)); } } } @@ -171,6 +152,6 @@ public class SingleCharAlternationInspection extends LocalInspectionTool { } } text.append("]"); - return RegExpReplacementUtil.escapeForContext(text.toString(), pattern); + return text.toString(); } } diff --git a/RegExpSupport/src/org/intellij/lang/regexp/psi/impl/RegExpElementImpl.java b/RegExpSupport/src/org/intellij/lang/regexp/psi/impl/RegExpElementImpl.java index 3204d24de4c0..87078ca50b3e 100644 --- a/RegExpSupport/src/org/intellij/lang/regexp/psi/impl/RegExpElementImpl.java +++ b/RegExpSupport/src/org/intellij/lang/regexp/psi/impl/RegExpElementImpl.java @@ -25,7 +25,7 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.impl.source.tree.CompositeElement; import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; -import com.intellij.psi.tree.IElementType; +import com.intellij.psi.tree.TokenSet; import com.intellij.util.IncorrectOperationException; import org.intellij.lang.regexp.RegExpLanguage; import org.intellij.lang.regexp.psi.RegExpElement; @@ -84,13 +84,15 @@ public abstract class RegExpElementImpl extends ASTWrapperPsiElement implements if (astNode == null) { return false; } + ASTNode child = null; if (astNode instanceof CompositeElement) { // in some languages token nodes are wrapped within a single-child composite ASTNode[] children = astNode.getChildren(null); - if (children.length != 1) return false; - astNode = children[0]; + if (children.length == 1) { + child = children[0]; + } } - final IElementType elementType = astNode.getElementType(); final ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(context.getLanguage()); - return parserDefinition.getStringLiteralElements().contains(elementType); + final TokenSet literalElements = parserDefinition.getStringLiteralElements(); + return literalElements.contains(astNode.getElementType()) || child != null && literalElements.contains(child.getElementType()); } } diff --git a/RegExpSupport/test/org/intellij/lang/regexp/inspection/SingleCharAlternationInspectionTest.java b/RegExpSupport/test/org/intellij/lang/regexp/inspection/SingleCharAlternationInspectionTest.java index 646dccd1efd5..216b5f580b3b 100644 --- a/RegExpSupport/test/org/intellij/lang/regexp/inspection/SingleCharAlternationInspectionTest.java +++ b/RegExpSupport/test/org/intellij/lang/regexp/inspection/SingleCharAlternationInspectionTest.java @@ -1,22 +1,7 @@ -/* - * Copyright 2000-2017 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. - */ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.intellij.lang.regexp.inspection; import com.intellij.codeInspection.LocalInspectionTool; -import com.intellij.testFramework.PsiTestUtil; import org.jetbrains.annotations.NotNull; /** @@ -38,7 +23,6 @@ public class SingleCharAlternationInspectionTest extends RegExpInspectionTestCas } public void testQuickfix() { - PsiTestUtil.disablePsiTextConsistencyChecks(getTestRootDisposable()); quickfixTest("x|y|z", "[xyz]", "Replace with '[xyz]'"); } @@ -47,12 +31,10 @@ public class SingleCharAlternationInspectionTest extends RegExpInspectionTestCas } public void testRemoveEscaping() { - PsiTestUtil.disablePsiTextConsistencyChecks(getTestRootDisposable()); quickfixTest("\\^|\\å|\\{|\\\\|\\[", "[\\^å{\\\\\\[]", "Replace with '[\\^å{\\\\\\[]'"); } public void testEscapes() { - PsiTestUtil.disablePsiTextConsistencyChecks(getTestRootDisposable()); quickfixTest("(\\.|\\[|]|\\(|\\)|\\{|}|\\^|\\?|\\*|\\||\\+|-|\\$)ab", "([.\\[\\](){}^?*|+\\-$])ab", "Replace with '[.\\[\\](){}^?*|+\\-$]'"); }