From 51acde88acb80e8cd4a4487b6d3e6a01504ba5f9 Mon Sep 17 00:00:00 2001 From: Bas Leijdekkers Date: Fri, 3 Feb 2017 12:05:40 +0100 Subject: [PATCH] RegExp: the first RegExp inspection --- RegExpSupport/src/META-INF/RegExpPlugin.xml | 3 + .../inspectionDescriptions/RepeatedSpace.html | 8 + .../inspection/RepeatedSpaceInspection.java | 157 ++++++++++++++++++ .../RepeatedSpaceInspectionTest.java | 69 ++++++++ 4 files changed, 237 insertions(+) create mode 100644 RegExpSupport/src/inspectionDescriptions/RepeatedSpace.html create mode 100644 RegExpSupport/src/org/intellij/lang/regexp/inspection/RepeatedSpaceInspection.java create mode 100644 RegExpSupport/test/org/intellij/lang/regexp/inspection/RepeatedSpaceInspectionTest.java diff --git a/RegExpSupport/src/META-INF/RegExpPlugin.xml b/RegExpSupport/src/META-INF/RegExpPlugin.xml index 10cded844ef9..a169960dfb3a 100644 --- a/RegExpSupport/src/META-INF/RegExpPlugin.xml +++ b/RegExpSupport/src/META-INF/RegExpPlugin.xml @@ -26,5 +26,8 @@ org.intellij.lang.regexp.intention.CheckRegExpIntentionAction RegExp + + diff --git a/RegExpSupport/src/inspectionDescriptions/RepeatedSpace.html b/RegExpSupport/src/inspectionDescriptions/RepeatedSpace.html new file mode 100644 index 000000000000..de84d704f38d --- /dev/null +++ b/RegExpSupport/src/inspectionDescriptions/RepeatedSpace.html @@ -0,0 +1,8 @@ + + +Reports multiple consecutive spaces in a RegExp. +Because spaces are not visible by default, it can be hard to see how many spaces are required. +The RegExp can be made more clear by replacing consecutive spaces with a single space and a counted quantifier. + + + \ No newline at end of file diff --git a/RegExpSupport/src/org/intellij/lang/regexp/inspection/RepeatedSpaceInspection.java b/RegExpSupport/src/org/intellij/lang/regexp/inspection/RepeatedSpaceInspection.java new file mode 100644 index 000000000000..686d50966c47 --- /dev/null +++ b/RegExpSupport/src/org/intellij/lang/regexp/inspection/RepeatedSpaceInspection.java @@ -0,0 +1,157 @@ +/* + * 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.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.IncorrectOperationException; +import org.intellij.lang.regexp.RegExpFileType; +import org.intellij.lang.regexp.psi.*; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +/** + * @author Bas Leijdekkers + */ +public class RepeatedSpaceInspection extends LocalInspectionTool { + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new RepeatedSpaceVisitor(holder); + } + + private static class RepeatedSpaceVisitor extends RegExpElementVisitor { + + private final ProblemsHolder myHolder; + private int myCount = 0; + private RegExpChar myFirstChar = null; + private boolean quoted = false; + + public RepeatedSpaceVisitor(ProblemsHolder holder) { + myHolder = holder; + } + + @Override + public void visitRegExpChar(RegExpChar aChar) { + if (!quoted && !(aChar.getParent() instanceof RegExpClass) && aChar.getType() == RegExpChar.Type.CHAR && aChar.getValue() == ' ') { + if (myFirstChar == null) { + myFirstChar = aChar; + } + myCount++; + } + else { + super.visitRegExpChar(aChar); + } + } + + @Override + public void visitWhiteSpace(PsiWhiteSpace space) { + super.visitWhiteSpace(space); + final String text = space.getText(); + if (text.equals("\\Q")) { + quoted = true; + } + else if (text.equals("\\E")) { + quoted = false; + } + myFirstChar = null; + myCount = 0; + } + + @Override + public void visitRegExpClass(RegExpClass expClass) { + super.visitRegExpClass(expClass); + myFirstChar = null; + myCount = 0; + } + + @Override + public void visitRegExpElement(RegExpElement element) { + super.visitRegExpElement(element); + if (myFirstChar != null && myCount > 1) { + final int offset = myFirstChar.getStartOffsetInParent(); + final String message = myCount + " consecutive spaces in RegExp"; + myHolder.registerProblem(myFirstChar.getParent(), new TextRange(offset, offset + myCount), message, + new RepeatedSpaceFix(myCount)); + } + myFirstChar = null; + myCount = 0; + } + } + + private static class RepeatedSpaceFix implements LocalQuickFix { + private final int myCount; + + public RepeatedSpaceFix(int count) { + myCount = count; + } + + @Nls + @NotNull + @Override + public String getName() { + return "Replace with ' {" + myCount + "}'"; + } + + @Nls + @NotNull + @Override + public String getFamilyName() { + return "Replace with space and repeated quantifier"; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + final PsiElement element = descriptor.getPsiElement(); + if (!(element instanceof RegExpBranch)) { + return; + } + final TextRange range = descriptor.getTextRangeInElement(); + final StringBuilder text = new StringBuilder(); + final PsiElement[] children = element.getChildren(); + boolean inserted = false; + for (PsiElement child : children) { + if (!range.contains(child.getStartOffsetInParent())) { + text.append(child.getText()); + } + else if (!inserted) { + text.append(" {").append(range.getLength()).append('}'); + inserted = true; + } + } + final RegExpPattern pattern = createPatternFromText(text, element); + final RegExpBranch branch = PsiTreeUtil.getChildOfType(pattern, RegExpBranch.class); + assert branch != null; + element.replace(branch); + } + + @NotNull + public static RegExpPattern createPatternFromText(@NotNull final CharSequence text, @NotNull final PsiElement context) { + final PsiFileFactory factory = PsiFileFactory.getInstance(context.getProject()); + final PsiFile file = factory.createFileFromText("dummy.regexp", RegExpFileType.INSTANCE, text); + final RegExpPattern pattern = PsiTreeUtil.getChildOfType(file, RegExpPattern.class); + assert pattern != null; + return pattern; + } + } +} diff --git a/RegExpSupport/test/org/intellij/lang/regexp/inspection/RepeatedSpaceInspectionTest.java b/RegExpSupport/test/org/intellij/lang/regexp/inspection/RepeatedSpaceInspectionTest.java new file mode 100644 index 000000000000..cb0c5015e30b --- /dev/null +++ b/RegExpSupport/test/org/intellij/lang/regexp/inspection/RepeatedSpaceInspectionTest.java @@ -0,0 +1,69 @@ +/* + * 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.codeInsight.intention.IntentionAction; +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase; +import org.intellij.lang.annotations.Language; +import org.intellij.lang.regexp.RegExpFileType; +import org.jetbrains.annotations.NotNull; +import org.junit.Assert; + +import java.util.List; + +/** + * @author Bas Leijdekkers + */ +public class RepeatedSpaceInspectionTest extends LightPlatformCodeInsightFixtureTestCase { + + public void testSimple() { + highlightTest(" "); + } + + public void testIgnoreQuoted() { + highlightTest("\\Q \\E"); + } + + public void testIgnoreInClass() { + highlightTest("[ ]"); + } + + public void testReplacement() { + quickfixTest(" ", " {5}", "Replace with ' {5}'"); + } + + private void highlightTest(@Language("RegExp") String code) { + myFixture.enableInspections(new RepeatedSpaceInspection()); + myFixture.configureByText(RegExpFileType.INSTANCE, code); + myFixture.testHighlighting(); + } + + private void quickfixTest(@Language("RegExp") String before, @Language("RegExp") String after, String hint) { + myFixture.enableInspections(new RepeatedSpaceInspection()); + myFixture.configureByText(RegExpFileType.INSTANCE, before); + final IntentionAction intention = findIntention(hint); + assertNotNull(intention); + myFixture.launchAction(intention); + myFixture.checkResult(after); + } + + public IntentionAction findIntention(@NotNull final String hint) { + final List intentions = myFixture.filterAvailableIntentions(hint); + Assert.assertFalse("\"" + hint + "\" not in " + intentions, intentions.isEmpty()); + Assert.assertFalse("Too many quickfixes found for \"" + hint + "\": " + intentions + "]", intentions.size() > 1); + return intentions.get(0); + } +}