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
This commit is contained in:
Bas Leijdekkers
2018-05-19 10:36:02 +02:00
parent 372b4d43a2
commit 26ddddac2b
6 changed files with 48 additions and 135 deletions
@@ -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));
}
}
}
@@ -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);
}
}
}
@@ -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<PsiElement> 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<PsiElement> 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);
}
}
@@ -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();
}
}
@@ -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());
}
}
@@ -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("<warning descr=\"Single character alternation in RegExp\">x|y|z</warning>", "[xyz]", "Replace with '[xyz]'");
}
@@ -47,12 +31,10 @@ public class SingleCharAlternationInspectionTest extends RegExpInspectionTestCas
}
public void testRemoveEscaping() {
PsiTestUtil.disablePsiTextConsistencyChecks(getTestRootDisposable());
quickfixTest("<warning descr=\"Single character alternation in RegExp\">\\^|\\å|\\{|\\\\|\\[</warning>", "[\\^å{\\\\\\[]", "Replace with '[\\^å{\\\\\\[]'");
}
public void testEscapes() {
PsiTestUtil.disablePsiTextConsistencyChecks(getTestRootDisposable());
quickfixTest("(<warning descr=\"Single character alternation in RegExp\">\\.|<caret>\\[|]|\\(|\\)|\\{|}|\\^|\\?|\\*|\\||\\+|-|\\$</warning>)ab",
"([.\\[\\](){}^?*|+\\-$])ab", "Replace with '[.\\[\\](){}^?*|+\\-$]'");
}