RegExp: new "Regular expression can be simplified" inspection (IDEA-286122)

Supersedes the "Asterisk closure can be collapsed to plus closure" inspection which was only available in PhpStorm

GitOrigin-RevId: a63c6fce88b3700cc521e8b113069a4b2b50a3e4
This commit is contained in:
Bas Leijdekkers
2022-01-13 20:26:25 +00:00
committed by intellij-monorepo-bot
parent b4afb2500e
commit ce078dac32
14 changed files with 474 additions and 160 deletions
@@ -77,5 +77,8 @@
<localInspection language="RegExp" shortName="RegExpSuspiciousBackref" enabledByDefault="true" level="WARNING"
bundle="messages.RegExpBundle" groupKey="inspection.group.name.regexp" key="inspection.name.suspicious.backref"
implementationClass="org.intellij.lang.regexp.inspection.SuspiciousBackrefInspection"/>
<localInspection language="RegExp" shortName="RegExpSimplifiable" enabledByDefault="true" level="WEAK WARNING"
bundle="messages.RegExpBundle" groupKey="inspection.group.name.regexp" key="inspection.name.simplifiable.expression"
implementationClass="org.intellij.lang.regexp.inspection.RegExpSimplifiableInspection"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,15 @@
<html>
<body>
Reports simplifiable regular expressions.
<p><b>Example:</b></p>
<pre><code>
[a] [0-9] xx* [ah-hz]
</code></pre>
<p>After the quick-fixes are applied:</p>
<pre><code>
a \d x+ [ahz]
</code></pre>
<!-- tooltip end -->
<p><small>New in 2022.1</small>
</body>
</html>
@@ -46,7 +46,6 @@ error.named.group.reference.not.allowed.inside.lookbehind=Named group reference
error.named.unicode.characters.are.not.allowed.in.this.regex.dialect=Named Unicode characters are not allowed in this regex dialect
error.nested.quantifier.in.regexp=Nested quantifier in regexp
error.property.escape.sequences.are.not.supported.in.this.regex.dialect=Property escape sequences are not supported in this regex dialect
error.redundant.character.range=Redundant character range
error.redundant.group.nesting=Redundant group nesting
error.repetition.value.too.large=Repetition value too large
error.this.boundary.is.not.supported.in.this.regex.dialect=This boundary is not supported in this regex dialect
@@ -75,6 +74,7 @@ inspection.name.escaped.meta.character=Escaped meta character
inspection.name.octal.escape=Octal escape
inspection.name.redundant.character.escape=Redundant character escape
inspection.name.redundant.nested.character.class=Redundant nested character class
inspection.name.simplifiable.expression=Regular expression can be simplified
inspection.name.single.character.alternation=Single character alternation
inspection.name.suspicious.backref=Suspicious back reference
inspection.name.unnecessary.non.capturing.group=Unnecessary non-capturing group
@@ -92,6 +92,8 @@ inspection.quick.fix.replace.with.hexadecimal.escape=Replace with hexadecimal es
inspection.quick.fix.replace.with.space.and.repeated.quantifier=Replace with space and repeated quantifier
inspection.warning.anchor.code.ref.code.in.unexpected.position=Anchor <code>#ref</code> in unexpected position
inspection.warning.anonymous.capturing.group=Anonymous capturing group
inspection.warning.can.be.removed=<code>#ref</code> is redundant
inspection.warning.can.be.simplified=<code>#ref</code> can be simplified to ''{0}''
inspection.warning.consecutive.spaces.in.regexp={0} consecutive spaces in RegExp
inspection.warning.duplicate.branch.in.alternation=Duplicate branch in alternation
inspection.warning.empty.branch.in.alternation=Empty branch in alternation
@@ -106,7 +108,6 @@ inspection.warning.redundant.nested.character.class=Redundant nested character c
inspection.warning.single.character.alternation.in.regexp=Single character alternation in RegExp
inspection.warning.unnecessary.non.capturing.group=Unnecessary non-capturing group <code>{0}</code>
intention.name.check.regexp=Check RegExp
intention.name.simplify.quantifier=Simplify quantifier
label.regexp=&RegExp:
label.sample=&Sample:
parse.error.category.shorthand.not.allowed.in.this.regular.expression.dialect=Category shorthand not allowed in this regular expression dialect
@@ -156,6 +157,3 @@ tooltip.no.match=Expression and example do not match
tooltip.pattern.is.too.complex=Regular expression pattern is too complex
warning.duplicate.character.0.inside.character.class=Duplicate character ''{0}'' inside character class
warning.duplicate.predefined.character.class.0.inside.character.class=Duplicate predefined character class ''{0}'' inside character class
weak.warning.fixed.repetition.range=Fixed repetition range
weak.warning.repetition.range.replaceable.by.0=Repetition range replaceable by ''{0}''
weak.warning.single.repetition=Single repetition
@@ -1,4 +1,4 @@
// Copyright 2000-2020 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.
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection;
import com.intellij.lang.injection.InjectedLanguageManager;
@@ -27,12 +27,22 @@ public final class RegExpReplacementUtil {
private RegExpReplacementUtil() {}
public static void replaceInContext(@NotNull PsiElement element, @NotNull String text) {
replaceInContext(element, text, null);
}
public static void replaceInContext(@NotNull PsiElement element, @NotNull String text, TextRange range) {
final PsiFile file = element.getContainingFile();
text = escapeForContext(text, file);
final Document document = file.getViewProvider().getDocument();
assert document != null;
final TextRange replaceRange = element.getTextRange();
document.replaceString(replaceRange.getStartOffset(), replaceRange.getEndOffset(), text);
final int startOffset = replaceRange.getStartOffset();
if (range != null) {
document.replaceString(startOffset + range.getStartOffset(), startOffset + range.getEndOffset(), text);
}
else {
document.replaceString(startOffset, replaceRange.getEndOffset(), text);
}
}
private static String escapeForContext(String text, PsiFile file) {
@@ -0,0 +1,317 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection;
import com.intellij.codeInspection.*;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.util.PsiTreeUtil;
import org.intellij.lang.regexp.RegExpBundle;
import org.intellij.lang.regexp.RegExpTT;
import org.intellij.lang.regexp.psi.*;
import org.jetbrains.annotations.NotNull;
/**
* @author Bas Leijdekkers
*/
public class RegExpSimplifiableInspection extends LocalInspectionTool {
@Override
public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder,
boolean isOnTheFly) {
return new RegExpSimplifiableVisitor(holder);
}
private static class RegExpSimplifiableVisitor extends RegExpElementVisitor {
private final ProblemsHolder myHolder;
RegExpSimplifiableVisitor(@NotNull ProblemsHolder holder) {
super();
myHolder = holder;
}
@Override
public void visitRegExpClass(RegExpClass regExpClass) {
super.visitRegExpClass(regExpClass);
final RegExpClassElement[] elements = regExpClass.getElements();
for (RegExpClassElement element : elements) {
if (element instanceof RegExpCharRange) {
final RegExpCharRange range = (RegExpCharRange)element;
final int from = range.getFrom().getValue();
final RegExpChar to = range.getTo();
if (from != -1 && to != null && from == to.getValue()) {
// [a-abc] -> [abc]
registerProblem(range, to.getUnescapedText());
}
}
}
if (regExpClass.isNegated()) {
if (elements.length == 1) {
final RegExpClassElement element = elements[0];
if (element instanceof RegExpSimpleClass) {
final RegExpSimpleClass simpleClass = (RegExpSimpleClass)element;
final String text = getInverseSimpleClassText(simpleClass);
if (text != null) {
// [^\d] -> \D
registerProblem(regExpClass, text);
}
}
else if (isDigitRange(element)) {
// [^0-9] -> \D
registerProblem(regExpClass, "\\D");
}
}
else {
if (isWordCharClassExpression(elements)) {
// [^0-9a-zA-Z_] -> \W
registerProblem(regExpClass, "\\W");
return;
}
for (RegExpClassElement element : elements) {
if (isDigitRange(element)) {
// [^0-9abc] -> [^\dabc]
registerProblem(element, "\\d");
}
}
}
}
else {
if (elements.length == 1) {
final RegExpClassElement element = elements[0];
if (!(element instanceof RegExpCharRange)) {
if (!(element instanceof RegExpChar) || !"{}().*+?|$".contains(element.getText())) {
// [a] -> a
registerProblem(regExpClass, element.getUnescapedText());
}
}
else {
if (isDigitRange(element)) {
// [0-9] -> \d
registerProblem(regExpClass, "\\d");
}
}
}
else {
if (isWordCharClassExpression(elements)) {
// [0-9a-zA-Z_] -> \w
registerProblem(regExpClass, "\\w");
return;
}
for (RegExpClassElement element : elements) {
// [0-9abc] -> [\dabc]
if (isDigitRange(element)) {
registerProblem(element, "\\d");
}
}
}
}
}
@Override
public void visitRegExpClosure(RegExpClosure closure) {
super.visitRegExpClosure(closure);
ASTNode token = closure.getQuantifier().getToken();
if (token == null || token.getElementType() != RegExpTT.STAR) {
return;
}
PsiElement sibling = closure.getPrevSibling();
RegExpAtom atom = closure.getAtom();
if (sibling instanceof RegExpElement && atom.getClass() == sibling.getClass() && sibling.textMatches(atom) && !containsGroup(atom)) {
final String text = atom.getUnescapedText() + '+';
myHolder.registerProblem(closure.getParent(),
TextRange.from(sibling.getStartOffsetInParent(), sibling.getTextLength() + closure.getTextLength()),
RegExpBundle.message("inspection.warning.can.be.simplified", text),
new RegExpSimplifiableFix(text));
}
}
@Override
public void visitRegExpProperty(RegExpProperty property) {
super.visitRegExpProperty(property);
final ASTNode categoryNode = property.getCategoryNode();
if (categoryNode == null) {
return;
}
final String category = categoryNode.getText();
if ("Digit".equals(category) || "IsDigit".equals(category)) {
registerProblem(property, property.isNegated() ? "\\D" : "\\d");
}
else if ("Blank".equals(category) || "IsBlank".equals(category)) {
registerProblem(property, property.isNegated() ? "[^ \\t]" : "[ \\t]");
}
else if ("Space".equals(category) || "IsSpace".equals(category) ||
"IsWhite_Space".equals(category) || "IsWhiteSpace".equals(category)) {
registerProblem(property, property.isNegated() ? "\\S" : "\\s");
}
}
@Override
public void visitRegExpQuantifier(RegExpQuantifier quantifier) {
if (!quantifier.isCounted()) {
return;
}
final RegExpNumber minElement = quantifier.getMin();
final String min = minElement == null ? "" : minElement.getText();
final RegExpNumber maxElement = quantifier.getMax();
final String max = maxElement == null ? "" : maxElement.getText();
if (!max.isEmpty() && max.equals(min)) {
if ("1".equals(max)) {
myHolder.registerProblem(quantifier,
RegExpBundle.message("inspection.warning.can.be.removed"),
new RegExpSimplifiableFix(quantifier.getText(), true));
}
else {
final ASTNode node = quantifier.getNode();
if (node.findChildByType(RegExpTT.COMMA) != null) {
registerProblem(quantifier, "{" + max + "}");
}
}
}
else if (("0".equals(min) || min.isEmpty()) && "1".equals(max)) {
registerProblem(quantifier, "?");
}
else if (("0".equals(min) || min.isEmpty()) && max.isEmpty()) {
registerProblem(quantifier, "*");
}
else if ("1".equals(min) && max.isEmpty()) {
registerProblem(quantifier, "+");
}
}
private void registerProblem(RegExpElement element, String replacement) {
myHolder.registerProblem(element,
RegExpBundle.message("inspection.warning.can.be.simplified", replacement),
new RegExpSimplifiableFix(replacement));
}
private static boolean containsGroup(RegExpAtom atom) {
return atom instanceof RegExpGroup || PsiTreeUtil.findChildOfType(atom, RegExpGroup.class) != null;
}
private static boolean isDigitRange(RegExpElement element) {
if (!(element instanceof RegExpCharRange)) {
return false;
}
final RegExpCharRange charRange = (RegExpCharRange)element;
final RegExpChar from = charRange.getFrom();
final RegExpChar to = charRange.getTo();
return from.getValue() == '0' && to != null && to.getValue() == '9';
}
private static boolean isWordCharClassExpression(RegExpClassElement[] elements) {
if (elements.length != 4) {
return false;
}
boolean lowerCaseChars = false;
boolean upperCaseChars = false;
boolean digits = false;
boolean underscore = false;
for (RegExpClassElement element : elements) {
if (element instanceof RegExpChar) {
final RegExpChar aChar = (RegExpChar)element;
if (aChar.getValue() == '_') {
underscore = true;
}
}
else if (element instanceof RegExpSimpleClass) {
final RegExpSimpleClass simpleClass = (RegExpSimpleClass)element;
if (simpleClass.getKind() == RegExpSimpleClass.Kind.DIGIT) {
digits = true;
}
}
else if (element instanceof RegExpCharRange) {
final RegExpCharRange range = (RegExpCharRange)element;
final RegExpChar from = range.getFrom();
final RegExpChar to = range.getTo();
if (to == null) {
break;
}
final int fromValue = from.getValue();
final int toValue = to.getValue();
if (fromValue == '0' && toValue == '9') {
digits = true;
}
else if (fromValue == 'A' && toValue == 'Z') {
upperCaseChars = true;
}
else if (fromValue == 'a' && toValue == 'z') {
lowerCaseChars = true;
}
}
}
return underscore && digits && lowerCaseChars && upperCaseChars;
}
private static String getInverseSimpleClassText(RegExpSimpleClass simpleClass) {
switch (simpleClass.getKind()) {
case DIGIT:
return "\\D";
case NON_DIGIT:
return "\\d";
case WORD:
return "\\W";
case NON_WORD:
return "\\w";
case SPACE:
return "\\S";
case NON_SPACE:
return "\\s";
case HORIZONTAL_SPACE:
return "\\H";
case NON_HORIZONTAL_SPACE:
return "\\h";
case VERTICAL_SPACE:
return "\\V";
case NON_VERTICAL_SPACE:
return "\\v";
case XML_NAME_START:
return "\\I";
case NON_XML_NAME_START:
return "\\i";
case XML_NAME_PART:
return "\\C";
case NON_XML_NAME_PART:
return "\\c";
default:
return null;
}
}
private static class RegExpSimplifiableFix implements LocalQuickFix {
private final String myExpression;
private final boolean myDelete;
RegExpSimplifiableFix(String newExpression) {
this(newExpression, false);
}
RegExpSimplifiableFix(String expression, boolean delete) {
myExpression = expression;
myDelete = delete;
}
@Override
public @NotNull String getFamilyName() {
return CommonQuickFixBundle.message("fix.simplify");
}
@Override
public @NotNull String getName() {
return myDelete
? CommonQuickFixBundle.message("fix.remove", myExpression)
: CommonQuickFixBundle.message("fix.replace.with.x", myExpression);
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
final PsiElement element = descriptor.getPsiElement();
if (!(element instanceof RegExpElement)) {
return;
}
RegExpReplacementUtil.replaceInContext(element, myDelete ? "" : myExpression, descriptor.getTextRangeInElement());
}
}
}
}
@@ -103,9 +103,6 @@ public final class RegExpAnnotator extends RegExpElementVisitor implements Annot
if (toCodePoint < fromCodePoint) {
myHolder.newAnnotation(HighlightSeverity.ERROR, RegExpBundle.message("error.illegal.character.range.to.from")).range(range).create();
}
else if (toCodePoint == fromCodePoint) {
myHolder.newAnnotation(HighlightSeverity.WARNING, RegExpBundle.message("error.redundant.character.range")).range(range).create();
}
}
@Override
@@ -368,34 +365,7 @@ public final class RegExpAnnotator extends RegExpElementVisitor implements Annot
public void visitRegExpQuantifier(RegExpQuantifier quantifier) {
if (quantifier.isCounted()) {
final RegExpNumber minElement = quantifier.getMin();
final String min = minElement == null ? "" : minElement.getText();
final RegExpNumber maxElement = quantifier.getMax();
final String max = maxElement == null ? "" : maxElement.getText();
if (!max.isEmpty() && max.equals(min)) {
if ("1".equals(max)) {
myHolder.newAnnotation(HighlightSeverity.WEAK_WARNING, RegExpBundle.message("weak.warning.single.repetition"))
.withFix(new SimplifyQuantifierAction(quantifier, null)).create();
}
else {
final ASTNode node = quantifier.getNode();
if (node.findChildByType(RegExpTT.COMMA) != null) {
myHolder.newAnnotation(HighlightSeverity.WEAK_WARNING, RegExpBundle.message("weak.warning.fixed.repetition.range"))
.withFix(new SimplifyQuantifierAction(quantifier, "{" + max + "}")).create();
}
}
}
else if (("0".equals(min) || min.isEmpty()) && "1".equals(max)) {
myHolder.newAnnotation(HighlightSeverity.WEAK_WARNING, RegExpBundle.message("weak.warning.repetition.range.replaceable.by.0", "?"))
.withFix(new SimplifyQuantifierAction(quantifier, "?")).create();
}
else if (("0".equals(min) || min.isEmpty()) && max.isEmpty()) {
myHolder.newAnnotation(HighlightSeverity.WEAK_WARNING, RegExpBundle.message("weak.warning.repetition.range.replaceable.by.0", "*"))
.withFix(new SimplifyQuantifierAction(quantifier, "*")).create();
}
else if ("1".equals(min) && max.isEmpty()) {
myHolder.newAnnotation(HighlightSeverity.WEAK_WARNING, RegExpBundle.message("weak.warning.repetition.range.replaceable.by.0", "+"))
.withFix(new SimplifyQuantifierAction(quantifier, "+")).create();
}
Number minValue = null;
if (minElement != null) {
minValue = myLanguageHosts.getQuantifierValue(minElement);
@@ -1,91 +0,0 @@
/*
* Copyright 2006 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.regexp.validation;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.CommonQuickFixBundle;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import org.intellij.lang.regexp.RegExpBundle;
import org.intellij.lang.regexp.RegExpFileType;
import org.intellij.lang.regexp.inspection.RegExpReplacementUtil;
import org.intellij.lang.regexp.psi.RegExpClosure;
import org.intellij.lang.regexp.psi.RegExpPattern;
import org.intellij.lang.regexp.psi.RegExpQuantifier;
import org.jetbrains.annotations.NotNull;
class SimplifyQuantifierAction implements IntentionAction {
private final RegExpQuantifier myQuantifier;
private final String myReplacement;
SimplifyQuantifierAction(RegExpQuantifier quantifier, String s) {
myQuantifier = quantifier;
myReplacement = s;
}
@Override
@NotNull
public String getText() {
return myReplacement == null ?
CommonQuickFixBundle.message("fix.remove", "{1,1}") :
CommonQuickFixBundle.message("fix.replace.with.x", myReplacement);
}
@Override
@NotNull
public String getFamilyName() {
return RegExpBundle.message("intention.name.simplify.quantifier");
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
return myQuantifier.isValid();
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
if (myReplacement == null) {
PsiElement parent = myQuantifier.getParent();
if (!(parent instanceof RegExpClosure)) {
return;
}
RegExpClosure closure = (RegExpClosure)parent;
RegExpReplacementUtil.replaceInContext(closure, closure.getAtom().getUnescapedText());
} else {
final PsiFileFactory factory = PsiFileFactory.getInstance(project);
final ASTNode modifier = myQuantifier.getModifier();
final PsiFile f = factory.createFileFromText("dummy.regexp", RegExpFileType.INSTANCE,
"a" + myReplacement + (modifier != null ? modifier.getText() : ""));
final RegExpPattern pattern = PsiTreeUtil.getChildOfType(f, RegExpPattern.class);
assert pattern != null;
final RegExpClosure closure = (RegExpClosure)pattern.getBranches()[0].getAtoms()[0];
myQuantifier.replace(closure.getQuantifier());
}
}
@Override
public boolean startInWriteAction() {
return true;
}
}
@@ -1,4 +1,4 @@
// 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.
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection;
import com.intellij.codeInspection.LocalInspectionTool;
@@ -15,7 +15,7 @@ public class DuplicateAlternationBranchInspectionTest extends RegExpInspectionTe
}
public void testMoreBranches() {
quickfixTest("<warning descr=\"Duplicate branch in alternation\">a{3}</warning>|<warning descr=\"Duplicate branch in alternation\">a<caret><weak_warning descr=\"Fixed repetition range\">{3,3}</weak_warning></warning>|b|c", "a{3}|b|c", "Remove duplicate branch");
quickfixTest("<warning descr=\"Duplicate branch in alternation\">a{3}</warning>|<warning descr=\"Duplicate branch in alternation\">a<caret>{3,3}</warning>|b|c", "a{3}|b|c", "Remove duplicate branch");
}
public void testOrderIrrelevant() {
@@ -0,0 +1,105 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.intellij.lang.regexp.inspection;
import com.intellij.codeInspection.CommonQuickFixBundle;
import com.intellij.codeInspection.LocalInspectionTool;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NotNull;
/**
* @author Bas Leijdekkers
*/
public class RegExpSimplifiableInspectionTest extends RegExpInspectionTestCase {
public void testRedundantRange() {
doTest("[ah-hz]", 2, 3, "h", "[ahz]");
}
public void testNegatedDigit() {
doTest("[^\\d]", "\\D");
}
public void testNegatedDigitRange() {
doTest("[^0-9]", "\\D");
}
public void testNegatedWordClassCharExpression() {
doTest("[^0-9a-zA-Z_]", "\\W");
}
public void testDigitRange() {
doTest("[^0-9abc]", 2, 3, "\\d", "[^\\dabc]");
}
public void testDigitRange2() {
doTest("[0-9abc]", 1, 3, "\\d", "[\\dabc]");
}
public void testSingleElementClass() {
doTest("[a]", "a");
}
public void testNoWarnSingleElementClass() {
highlightTest("[.]");
}
public void testSimpleDigitRange() {
doTest("[0-9]", "\\d");
}
public void testWordCharClassExpression() {
doTest("[0-9a-zA-Z_]", "\\w");
}
public void testStarToPlusNoWarm() {
highlightTest("bba*c");
}
public void testStarToPlusNoWarn2() {
highlightTest("b(a)(a)*c");
}
public void testStarToPlus() {
doTest("baa*c", 1, 3, "a+", "ba+c");
}
public void testSingleRepetition() {
quickfixTest("a<weak_warning descr=\"'{1}' is redundant\"><caret>{1}</weak_warning>",
"a", CommonQuickFixBundle.message("fix.remove", "{1}"));
}
public void testSimplifiableRange1() {
doTest("a{0,1}", 1, 5, "?", "a?");
}
public void testSimplifiableRange2() {
doTest("a{1,}", 1, 4, "+", "a+");
}
public void testSimplifiableRange3() {
doTest("a{0,}", 1, 4, "*", "a*");
}
public void testFixedRepetitionRange() {
doTest("a{3,3}", 1, 5, "{3}", "a{3}");
}
private void doTest(@Language("RegExp") String code, @Language("RegExp") String replacement) {
doTest(code, 0, code.length(), replacement, replacement);
}
private void doTest(@Language("RegExp") String code, int offset, int length,
String replacement,
@Language("RegExp") String result) {
final String suspect = code.substring(offset, offset + length);
@Language("RegExp") final String warning =
code.substring(0, offset) + "<weak_warning descr=\"'" + suspect + "' can be simplified to '" + replacement + "'\"><caret>" +
suspect + "</weak_warning>" + code.substring(offset + length);
quickfixTest(warning, result, CommonQuickFixBundle.message("fix.replace.with.x", replacement));
}
@Override
protected @NotNull LocalInspectionTool getInspection() {
return new RegExpSimplifiableInspection();
}
}
@@ -2,11 +2,11 @@ class S {
@SuppressWarnings("Annotator")
void foo() {
//language=RegExp
String regexp = "a{1,}";
String regexp = "a()";
}
@SuppressWarnings("Annotator")
void bar() {
String regexp = "a{1,}";
String regexp = "a()";
}
}
@@ -1,4 +1,4 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInsight;
import com.intellij.ide.highlighter.JavaFileType;
@@ -8,6 +8,7 @@ import com.intellij.testFramework.IdeaTestUtil;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.intellij.lang.regexp.inspection.AnonymousGroupInspection;
import org.intellij.lang.regexp.inspection.RegExpSimplifiableInspection;
import org.intellij.lang.regexp.inspection.UnexpectedAnchorInspection;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
@@ -26,8 +27,12 @@ public class RegExpHighlightingTest extends LightJavaCodeInsightFixtureTestCase
doTest("<warning descr=\"Anonymous capturing group\">(</warning>moo)<warning descr=\"Numeric back reference\">\\1</warning>");
}
public void testSingleRepetition() {
doTest("a<weak_warning descr=\"Single repetition\">{1}</weak_warning>");
public void testWhiteSpaceProperty() {
// needs only partial escaping
@NonNls String code = "<weak_warning descr=\"'\\\\P{IsBlank}' can be simplified to '[^ \\t]'\">\\\\P{IsBlank}</weak_warning>";
myFixture.enableInspections(new RegExpSimplifiableInspection());
myFixture.configureByText(JavaFileType.INSTANCE, "class X {{ java.util.regex.Pattern.compile(\"" + code + "\"); }}");
myFixture.testHighlighting();
}
public void testRedundantEscape1() {
@@ -40,22 +45,6 @@ public class RegExpHighlightingTest extends LightJavaCodeInsightFixtureTestCase
doTest("\\b \\b{g} \\B \\A \\z \\Z \\G");
}
public void testSimplifiableRange1() {
doTest("a<weak_warning descr=\"Repetition range replaceable by '?'\">{0,1}</weak_warning>");
}
public void testSimplifiableRange2() {
doTest("a<weak_warning descr=\"Repetition range replaceable by '+'\">{1,}</weak_warning>");
}
public void testSimplifiableRange3() {
doTest("a<weak_warning descr=\"Repetition range replaceable by '*'\">{0,}</weak_warning>");
}
public void testFixedRepetitionRange() {
doTest("a<weak_warning descr=\"Fixed repetition range\">{3,3}</weak_warning>");
}
public void testNotDuplicateControlCharacter() {
doTest("[\\ca\\cb]");
}
@@ -110,10 +99,6 @@ public class RegExpHighlightingTest extends LightJavaCodeInsightFixtureTestCase
doTest("(?<importantValue1>\\d\\d)");
}
public void testRedundantCharacterRange() {
doTest("[<warning descr=\"Redundant character range\">a-a</warning>]");
}
public void testIllegalCharacterRange1() {
doTest("[<error descr=\"Illegal character range (to < from)\">\\x4a-\\x3f</error>]");
}
@@ -210,7 +195,6 @@ public class RegExpHighlightingTest extends LightJavaCodeInsightFixtureTestCase
doTest("a{2147483647}");
doTest("a{<error descr=\"Repetition value too large\">2147483648</error>}");
doTest("a{<error descr=\"Illegal repetition range (min > max)\">1,0</error>}");
doTest("a<weak_warning descr=\"Repetition range replaceable by '*'\">{<error descr=\"Number expected\">,</error>}</weak_warning>");
}
public void testOptions() {
+2 -1
View File
@@ -5,4 +5,5 @@ re.compile(r'a{4294967294}')
re.compile(r'a{<error descr="Repetition value too large">4294967295</error>,1}')
re.compile(r'a{1,<error descr="Repetition value too large">4294967295</error>}')
re.compile(r'a{<error descr="Illegal repetition range (min > max)">2,1</error>}')
re.compile(r'a{1,2}<error descr="Nested quantifier in regexp">+</error>')
re.compile(r'a{1,2}<error descr="Nested quantifier in regexp">+</error>')
re.compile(r'a<weak_warning descr="'{,}' can be simplified to '*'">{,}</weak_warning>')
+2 -2
View File
@@ -3,9 +3,9 @@ import re
re.compile(r"(?<!a|b)");
re.compile(r"(?<!<error descr="Alternation alternatives need to have the same length inside lookbehind">a|bc</error>)");
re.compile(r"(?<!a{3}})");
re.compile(r"(?<!a<weak_warning descr="Fixed repetition range">{3,3}</weak_warning>})");
re.compile(r"(?<!a{3,3}})");
re.compile(r"(?<!a<error descr="Unequal min and max in counted quantifier not allowed inside lookbehind">{3,4}</error>})");
re.compile(r"(?<!a|b<weak_warning descr="Single repetition">{1}</weak_warning>)")
re.compile(r"(?<!a|b{1})")
re.compile(r"(?<!abcd|(ab){2})")
re.compile(r"(?<!abcde|x(ab){2})")
re.compile(r"(?<!a(ab|cd)|xyz)")
@@ -1,4 +1,4 @@
// Copyright 2000-2021 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.
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python;
import com.intellij.lang.injection.InjectedLanguageManager;
@@ -14,6 +14,7 @@ import com.jetbrains.python.codeInsight.regexp.PythonVerboseRegexpParserDefiniti
import com.jetbrains.python.fixtures.PyLexerTestCase;
import com.jetbrains.python.fixtures.PyTestCase;
import org.intellij.lang.regexp.inspection.RegExpRedundantEscapeInspection;
import org.intellij.lang.regexp.inspection.RegExpSimplifiableInspection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -44,6 +45,7 @@ public class PyRegexpTest extends PyTestCase {
}
public void testCountedQuantifier() {
myFixture.enableInspections(new RegExpSimplifiableInspection());
doTestHighlighting();
}