mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
b4afb2500e
commit
ce078dac32
@@ -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) {
|
||||
|
||||
+317
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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() {
|
||||
|
||||
+105
@@ -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() {
|
||||
|
||||
@@ -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>')
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user