diff --git a/RegExpSupport/src/META-INF/RegExpPlugin.xml b/RegExpSupport/src/META-INF/RegExpPlugin.xml
index f34ef8958cb1..ac3cfebb9be0 100644
--- a/RegExpSupport/src/META-INF/RegExpPlugin.xml
+++ b/RegExpSupport/src/META-INF/RegExpPlugin.xml
@@ -45,5 +45,8 @@
+
diff --git a/RegExpSupport/src/inspectionDescriptions/AnonymousGroup.html b/RegExpSupport/src/inspectionDescriptions/AnonymousGroup.html
new file mode 100644
index 000000000000..62a4c3ef170a
--- /dev/null
+++ b/RegExpSupport/src/inspectionDescriptions/AnonymousGroup.html
@@ -0,0 +1,11 @@
+
+
+Reports anonymous capturing groups and numeric back references in a RegExp.
+These are only reported when the RegExp dialect supports named group and named group references.
+Named groups and named back references improve code readability and are recommended to use instead.
+When a capture is not needed, matching can be more performant and use less memory by using a non-capturing group,
+i.e. (?:xxx) instead of (xxx).
+
+New in 2017.2
+
+
\ No newline at end of file
diff --git a/RegExpSupport/src/org/intellij/lang/regexp/RegExpLanguageHost.java b/RegExpSupport/src/org/intellij/lang/regexp/RegExpLanguageHost.java
index 61f3d12ac341..f21add85d2ee 100644
--- a/RegExpSupport/src/org/intellij/lang/regexp/RegExpLanguageHost.java
+++ b/RegExpSupport/src/org/intellij/lang/regexp/RegExpLanguageHost.java
@@ -20,16 +20,25 @@ import org.intellij.lang.regexp.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.util.EnumSet;
+
/**
* @author yole
*/
public interface RegExpLanguageHost {
+
+ EnumSet EMPTY_NAMED_GROUP_TYPES = EnumSet.noneOf(RegExpGroup.Type.class);
+
boolean characterNeedsEscaping(char c);
boolean supportsPerl5EmbeddedComments();
boolean supportsPossessiveQuantifiers();
boolean supportsPythonConditionalRefs();
boolean supportsNamedGroupSyntax(RegExpGroup group);
boolean supportsNamedGroupRefSyntax(RegExpNamedGroupRef ref);
+ @NotNull
+ default EnumSet getSupportedNamedGroupTypes(RegExpElement context) {
+ return EMPTY_NAMED_GROUP_TYPES;
+ }
boolean supportsExtendedHexCharacter(RegExpChar regExpChar);
default boolean isValidGroupName(String name, @NotNull PsiElement context) {
@@ -116,7 +125,7 @@ public interface RegExpLanguageHost {
/** Finite repetition inside lookbehind with different minimum, maximum values allowed */
FINITE_REPETITION,
- /** Full regex syntax inside lookbehind ,i.e. star (*) and plus (*) repetition and backreferences, allowed. */
+ /** Full regex syntax inside lookbehind, i.e. star (*) and plus (*) repetition and backreferences, allowed. */
FULL
}
}
diff --git a/RegExpSupport/src/org/intellij/lang/regexp/RegExpLanguageHosts.java b/RegExpSupport/src/org/intellij/lang/regexp/RegExpLanguageHosts.java
index e9d43a4f3bcd..8f1977230d59 100644
--- a/RegExpSupport/src/org/intellij/lang/regexp/RegExpLanguageHosts.java
+++ b/RegExpSupport/src/org/intellij/lang/regexp/RegExpLanguageHosts.java
@@ -24,6 +24,9 @@ import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.util.Collection;
+import java.util.Collections;
+
/**
* @author yole
*/
@@ -106,6 +109,14 @@ public final class RegExpLanguageHosts extends ClassExtension getSupportedNamedGroupTypes(RegExpElement context) {
+ final RegExpLanguageHost host = findRegExpHost(context);
+ if (host == null) {
+ return Collections.emptySet();
+ }
+ return host.getSupportedNamedGroupTypes(context);
+ }
+
public boolean isValidGroupName(String name, @Nullable final PsiElement context) {
final RegExpLanguageHost host = findRegExpHost(context);
return host != null && host.isValidGroupName(name, context);
diff --git a/RegExpSupport/src/org/intellij/lang/regexp/inspection/AnonymousGroupInspection.java b/RegExpSupport/src/org/intellij/lang/regexp/inspection/AnonymousGroupInspection.java
new file mode 100644
index 000000000000..e2749042960b
--- /dev/null
+++ b/RegExpSupport/src/org/intellij/lang/regexp/inspection/AnonymousGroupInspection.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.intellij.lang.regexp.inspection;
+
+import com.intellij.codeInspection.LocalInspectionTool;
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.psi.PsiElementVisitor;
+import org.intellij.lang.regexp.RegExpLanguageHosts;
+import org.intellij.lang.regexp.psi.RegExpBackref;
+import org.intellij.lang.regexp.psi.RegExpElementVisitor;
+import org.intellij.lang.regexp.psi.RegExpGroup;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+
+/**
+ * @author Bas Leijdekkers
+ */
+public class AnonymousGroupInspection extends LocalInspectionTool {
+
+ @Nls
+ @NotNull
+ @Override
+ public String getDisplayName() {
+ return "Anonymous capturing group or numeric back reference";
+ }
+
+ @NotNull
+ @Override
+ public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
+ return new AnonymousGroupVisitor(holder);
+ }
+
+ private static class AnonymousGroupVisitor extends RegExpElementVisitor {
+
+ private final ProblemsHolder myHolder;
+
+ public AnonymousGroupVisitor(ProblemsHolder holder) {
+ myHolder = holder;
+ }
+
+ @Override
+ public void visitRegExpGroup(RegExpGroup group) {
+ if (group.getType() != RegExpGroup.Type.CAPTURING_GROUP) {
+ return;
+ }
+ final Collection types = RegExpLanguageHosts.getInstance().getSupportedNamedGroupTypes(group);
+ if (types.isEmpty()) {
+ return;
+ }
+ myHolder.registerProblem(group.getFirstChild(), "Anonymous capturing group");
+ }
+
+ @Override
+ public void visitRegExpBackref(RegExpBackref backref) {
+ final Collection types = RegExpLanguageHosts.getInstance().getSupportedNamedGroupTypes(backref);
+ if (types.isEmpty()) {
+ return;
+ }
+ myHolder.registerProblem(backref, "Numeric back reference");
+ }
+ }
+}
diff --git a/java/java-impl/src/com/intellij/psi/impl/JavaRegExpHost.java b/java/java-impl/src/com/intellij/psi/impl/JavaRegExpHost.java
index c14300e573a1..2e745a2669f5 100644
--- a/java/java-impl/src/com/intellij/psi/impl/JavaRegExpHost.java
+++ b/java/java-impl/src/com/intellij/psi/impl/JavaRegExpHost.java
@@ -31,6 +31,7 @@ import org.intellij.lang.regexp.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.util.EnumSet;
import java.util.Locale;
/**
@@ -38,6 +39,7 @@ import java.util.Locale;
*/
public class JavaRegExpHost implements RegExpLanguageHost {
+ protected static final EnumSet SUPPORTED_NAMED_GROUP_TYPES = EnumSet.of(RegExpGroup.Type.NAMED_GROUP);
private final DefaultRegExpPropertiesProvider myPropertiesProvider;
private final String[][] myPropertyNames = {
@@ -171,6 +173,15 @@ public class JavaRegExpHost implements RegExpLanguageHost {
return ref.isNamedGroupRef() && hasAtLeastJdkVersion(ref, JavaSdkVersion.JDK_1_7);
}
+ @NotNull
+ @Override
+ public EnumSet getSupportedNamedGroupTypes(RegExpElement context) {
+ if (!hasAtLeastJdkVersion(context, JavaSdkVersion.JDK_1_7)) {
+ return EMPTY_NAMED_GROUP_TYPES;
+ }
+ return SUPPORTED_NAMED_GROUP_TYPES;
+ }
+
@Override
public boolean isValidGroupName(String name, @NotNull PsiElement context) {
for (int i = 0, length = name.length(); i < length; i++) {
diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/RegExpHighlightingTest.java b/java/java-tests/testSrc/com/intellij/codeInsight/RegExpHighlightingTest.java
index 222877772b18..0d3f44eb8833 100644
--- a/java/java-tests/testSrc/com/intellij/codeInsight/RegExpHighlightingTest.java
+++ b/java/java-tests/testSrc/com/intellij/codeInsight/RegExpHighlightingTest.java
@@ -22,6 +22,7 @@ import com.intellij.testFramework.IdeaTestUtil;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
import org.intellij.lang.annotations.Language;
+import org.intellij.lang.regexp.inspection.AnonymousGroupInspection;
import org.jetbrains.annotations.NotNull;
/**
@@ -30,6 +31,11 @@ import org.jetbrains.annotations.NotNull;
@SuppressWarnings("Annotator")
public class RegExpHighlightingTest extends LightCodeInsightFixtureTestCase {
+ public void testAnonymousCapturingGroupInspection() {
+ myFixture.enableInspections(new AnonymousGroupInspection());
+ doTest("(moo)\\1");
+ }
+
public void testSingleRepetition() {
doTest("a{1}");
}
diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/regexp/GroovyRegExpHost.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/regexp/GroovyRegExpHost.java
index 57e5071e7d9c..c1f1b98b78ac 100644
--- a/plugins/groovy/src/org/jetbrains/plugins/groovy/regexp/GroovyRegExpHost.java
+++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/regexp/GroovyRegExpHost.java
@@ -17,13 +17,21 @@ package org.jetbrains.plugins.groovy.regexp;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
+import com.intellij.openapi.projectRoots.JavaSdkVersion;
import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.JavaRegExpHost;
+import org.intellij.lang.regexp.RegExpLanguageHost;
import org.intellij.lang.regexp.psi.RegExpChar;
+import org.intellij.lang.regexp.psi.RegExpElement;
import org.intellij.lang.regexp.psi.RegExpGroup;
import org.intellij.lang.regexp.psi.RegExpNamedGroupRef;
+import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.groovy.config.GroovyConfigUtils;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+
/**
* @author Bas Leijdekkers
*/
@@ -47,6 +55,16 @@ public class GroovyRegExpHost extends JavaRegExpHost {
return false;
}
+ @NotNull
+ @Override
+ public EnumSet getSupportedNamedGroupTypes(RegExpElement context) {
+ final String version = getGroovyVersion(context);
+ if (version == null || version.compareTo(GroovyConfigUtils.GROOVY2_0) < 0) {
+ return EMPTY_NAMED_GROUP_TYPES;
+ }
+ return SUPPORTED_NAMED_GROUP_TYPES;
+ }
+
@Override
public boolean supportsExtendedHexCharacter(RegExpChar regExpChar) {
final String version = getGroovyVersion(regExpChar);
diff --git a/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java
index f548ac0e93f9..e454bbcd9d63 100644
--- a/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java
+++ b/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java
@@ -34,10 +34,7 @@ import com.jetbrains.python.psi.types.PyType;
import com.jetbrains.python.psi.types.TypeEvalContext;
import org.intellij.lang.regexp.DefaultRegExpPropertiesProvider;
import org.intellij.lang.regexp.RegExpLanguageHost;
-import org.intellij.lang.regexp.psi.RegExpChar;
-import org.intellij.lang.regexp.psi.RegExpGroup;
-import org.intellij.lang.regexp.psi.RegExpNamedGroupRef;
-import org.intellij.lang.regexp.psi.RegExpNumber;
+import org.intellij.lang.regexp.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -458,6 +455,12 @@ public class PyStringLiteralExpressionImpl extends PyElementImpl implements PySt
return ref.isPythonNamedGroupRef();
}
+ @NotNull
+ @Override
+ public EnumSet getSupportedNamedGroupTypes(RegExpElement context) {
+ return EnumSet.of(RegExpGroup.Type.PYTHON_NAMED_GROUP);
+ }
+
@Override
public boolean supportsExtendedHexCharacter(RegExpChar regExpChar) {
return false;