From ce078dac325e9c7e8ed8887add181a912b06dee5 Mon Sep 17 00:00:00 2001 From: Bas Leijdekkers Date: Wed, 12 Jan 2022 17:51:32 +0100 Subject: [PATCH] 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 --- .../resources/META-INF/RegExpPlugin.xml | 3 + .../RegExpSimplifiable.html | 15 + .../messages/RegExpBundle.properties | 8 +- .../inspection/RegExpReplacementUtil.java | 14 +- .../RegExpSimplifiableInspection.java | 317 ++++++++++++++++++ .../regexp/validation/RegExpAnnotator.java | 30 -- .../validation/SimplifyQuantifierAction.java | 91 ----- ...licateAlternationBranchInspectionTest.java | 4 +- .../RegExpSimplifiableInspectionTest.java | 105 ++++++ .../redundantSuppress/annotator/src/X.java | 4 +- .../codeInsight/RegExpHighlightingTest.java | 32 +- python/testData/regexp/countedQuantifier.py | 3 +- python/testData/regexp/lookbehind.py | 4 +- .../com/jetbrains/python/PyRegexpTest.java | 4 +- 14 files changed, 474 insertions(+), 160 deletions(-) create mode 100644 RegExpSupport/resources/inspectionDescriptions/RegExpSimplifiable.html create mode 100644 RegExpSupport/src/org/intellij/lang/regexp/inspection/RegExpSimplifiableInspection.java delete mode 100644 RegExpSupport/src/org/intellij/lang/regexp/validation/SimplifyQuantifierAction.java create mode 100644 RegExpSupport/test/org/intellij/lang/regexp/inspection/RegExpSimplifiableInspectionTest.java diff --git a/RegExpSupport/resources/META-INF/RegExpPlugin.xml b/RegExpSupport/resources/META-INF/RegExpPlugin.xml index fcfc8b35f2f0..c2a96352bdd1 100644 --- a/RegExpSupport/resources/META-INF/RegExpPlugin.xml +++ b/RegExpSupport/resources/META-INF/RegExpPlugin.xml @@ -77,5 +77,8 @@ + diff --git a/RegExpSupport/resources/inspectionDescriptions/RegExpSimplifiable.html b/RegExpSupport/resources/inspectionDescriptions/RegExpSimplifiable.html new file mode 100644 index 000000000000..e690a84ae0ee --- /dev/null +++ b/RegExpSupport/resources/inspectionDescriptions/RegExpSimplifiable.html @@ -0,0 +1,15 @@ + + +Reports simplifiable regular expressions. +

Example:

+

+  [a] [0-9] xx* [ah-hz]
+
+

After the quick-fixes are applied:

+

+  a \d x+ [ahz]
+
+ +

New in 2022.1 + + \ No newline at end of file diff --git a/RegExpSupport/resources/messages/RegExpBundle.properties b/RegExpSupport/resources/messages/RegExpBundle.properties index 2613f5e83cee..97c151baecb2 100644 --- a/RegExpSupport/resources/messages/RegExpBundle.properties +++ b/RegExpSupport/resources/messages/RegExpBundle.properties @@ -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 #ref in unexpected position inspection.warning.anonymous.capturing.group=Anonymous capturing group +inspection.warning.can.be.removed=#ref is redundant +inspection.warning.can.be.simplified=#ref 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 {0} 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 diff --git a/RegExpSupport/src/org/intellij/lang/regexp/inspection/RegExpReplacementUtil.java b/RegExpSupport/src/org/intellij/lang/regexp/inspection/RegExpReplacementUtil.java index 584e96eee881..fb4313a3c568 100644 --- a/RegExpSupport/src/org/intellij/lang/regexp/inspection/RegExpReplacementUtil.java +++ b/RegExpSupport/src/org/intellij/lang/regexp/inspection/RegExpReplacementUtil.java @@ -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) { diff --git a/RegExpSupport/src/org/intellij/lang/regexp/inspection/RegExpSimplifiableInspection.java b/RegExpSupport/src/org/intellij/lang/regexp/inspection/RegExpSimplifiableInspection.java new file mode 100644 index 000000000000..e42b0abfd088 --- /dev/null +++ b/RegExpSupport/src/org/intellij/lang/regexp/inspection/RegExpSimplifiableInspection.java @@ -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()); + } + } + } +} diff --git a/RegExpSupport/src/org/intellij/lang/regexp/validation/RegExpAnnotator.java b/RegExpSupport/src/org/intellij/lang/regexp/validation/RegExpAnnotator.java index f022e15f81b6..5173aa9334bc 100644 --- a/RegExpSupport/src/org/intellij/lang/regexp/validation/RegExpAnnotator.java +++ b/RegExpSupport/src/org/intellij/lang/regexp/validation/RegExpAnnotator.java @@ -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); diff --git a/RegExpSupport/src/org/intellij/lang/regexp/validation/SimplifyQuantifierAction.java b/RegExpSupport/src/org/intellij/lang/regexp/validation/SimplifyQuantifierAction.java deleted file mode 100644 index d0897b676697..000000000000 --- a/RegExpSupport/src/org/intellij/lang/regexp/validation/SimplifyQuantifierAction.java +++ /dev/null @@ -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; - } -} diff --git a/RegExpSupport/test/org/intellij/lang/regexp/inspection/DuplicateAlternationBranchInspectionTest.java b/RegExpSupport/test/org/intellij/lang/regexp/inspection/DuplicateAlternationBranchInspectionTest.java index a0c5c08ed1d7..21c68d872920 100644 --- a/RegExpSupport/test/org/intellij/lang/regexp/inspection/DuplicateAlternationBranchInspectionTest.java +++ b/RegExpSupport/test/org/intellij/lang/regexp/inspection/DuplicateAlternationBranchInspectionTest.java @@ -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("a{3}|a{3,3}|b|c", "a{3}|b|c", "Remove duplicate branch"); + quickfixTest("a{3}|a{3,3}|b|c", "a{3}|b|c", "Remove duplicate branch"); } public void testOrderIrrelevant() { diff --git a/RegExpSupport/test/org/intellij/lang/regexp/inspection/RegExpSimplifiableInspectionTest.java b/RegExpSupport/test/org/intellij/lang/regexp/inspection/RegExpSimplifiableInspectionTest.java new file mode 100644 index 000000000000..69dfd8afcac0 --- /dev/null +++ b/RegExpSupport/test/org/intellij/lang/regexp/inspection/RegExpSimplifiableInspectionTest.java @@ -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{1}", + "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) + "" + + suspect + "" + code.substring(offset + length); + quickfixTest(warning, result, CommonQuickFixBundle.message("fix.replace.with.x", replacement)); + } + + @Override + protected @NotNull LocalInspectionTool getInspection() { + return new RegExpSimplifiableInspection(); + } +} diff --git a/java/java-tests/testData/inspection/redundantSuppress/annotator/src/X.java b/java/java-tests/testData/inspection/redundantSuppress/annotator/src/X.java index 138e73d6301a..a9dbdadc926f 100644 --- a/java/java-tests/testData/inspection/redundantSuppress/annotator/src/X.java +++ b/java/java-tests/testData/inspection/redundantSuppress/annotator/src/X.java @@ -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()"; } } \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/RegExpHighlightingTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/RegExpHighlightingTest.java index 7fbc421b6c74..8143b7918c39 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/RegExpHighlightingTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/RegExpHighlightingTest.java @@ -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("(moo)\\1"); } - public void testSingleRepetition() { - doTest("a{1}"); + public void testWhiteSpaceProperty() { + // needs only partial escaping + @NonNls String code = "\\\\P{IsBlank}"; + 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{0,1}"); - } - - public void testSimplifiableRange2() { - doTest("a{1,}"); - } - - public void testSimplifiableRange3() { - doTest("a{0,}"); - } - - public void testFixedRepetitionRange() { - doTest("a{3,3}"); - } - public void testNotDuplicateControlCharacter() { doTest("[\\ca\\cb]"); } @@ -110,10 +99,6 @@ public class RegExpHighlightingTest extends LightJavaCodeInsightFixtureTestCase doTest("(?\\d\\d)"); } - public void testRedundantCharacterRange() { - doTest("[a-a]"); - } - public void testIllegalCharacterRange1() { doTest("[\\x4a-\\x3f]"); } @@ -210,7 +195,6 @@ public class RegExpHighlightingTest extends LightJavaCodeInsightFixtureTestCase doTest("a{2147483647}"); doTest("a{2147483648}"); doTest("a{ max)\">1,0}"); - doTest("a{,}"); } public void testOptions() { diff --git a/python/testData/regexp/countedQuantifier.py b/python/testData/regexp/countedQuantifier.py index 22e3f1d8fa20..35fa63104045 100644 --- a/python/testData/regexp/countedQuantifier.py +++ b/python/testData/regexp/countedQuantifier.py @@ -5,4 +5,5 @@ re.compile(r'a{4294967294}') re.compile(r'a{4294967295,1}') re.compile(r'a{1,4294967295}') re.compile(r'a{2,1}') -re.compile(r'a{1,2}+') \ No newline at end of file +re.compile(r'a{1,2}+') +re.compile(r'a{,}') \ No newline at end of file diff --git a/python/testData/regexp/lookbehind.py b/python/testData/regexp/lookbehind.py index e40a1fbe1355..160a0f28a8fe 100644 --- a/python/testData/regexp/lookbehind.py +++ b/python/testData/regexp/lookbehind.py @@ -3,9 +3,9 @@ import re re.compile(r"(?a|bc)"); re.compile(r"(?{3,3}})"); +re.compile(r"(?{3,4}})"); -re.compile(r"(?{1})") +re.compile(r"(?