[groovy] make semicolon necessary by default (IDEA-192360)

- highlight only known unnecessary semicolon cases;
- move messages to bundle;
- extract the fix to separate class.
This commit is contained in:
Daniil Ovchinnikov
2018-05-21 16:50:29 +03:00
parent ae1d293b4b
commit f182f373bb
6 changed files with 157 additions and 102 deletions
@@ -123,4 +123,6 @@ unnecessary.modifier.description=Modifier ''{0}'' is not necessary
unnecessary.modifier.remove=Remove unnecessary ''{0}''
unnecessary.def.explicitly.typed.only=Only report in explicitly typed declarations
unnecessary.alias.fix=Remove unnecessary alias
unnecessary.alias.description=Alias is unnecessary
unnecessary.alias.description=Alias is unnecessary
unnecessary.semicolon.description=Semicolon is unnecessary
unnecessary.semicolon.fix=Remove semicolon
@@ -0,0 +1,19 @@
// 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.
package org.jetbrains.plugins.groovy.codeInspection.fixes
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import com.intellij.psi.codeStyle.CodeStyleManager
class RemoveElementWithoutFormatterFix(private val familyName: String) : LocalQuickFix {
override fun getFamilyName(): String = familyName
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement ?: return
CodeStyleManager.getInstance(project).performActionWithFormatterDisabled {
element.delete()
}
}
}
@@ -1,84 +0,0 @@
/*
* Copyright 2000-2016 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.jetbrains.plugins.groovy.codeInspection.style;
import com.intellij.codeInspection.*;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.tree.TokenSet;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.groovy.codeInspection.GroovySuppressableInspectionTool;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.clauses.GrTraditionalForClause;
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;
import static com.intellij.psi.tree.TokenSet.andNot;
import static com.intellij.psi.tree.TokenSet.orSet;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mNLS;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mSEMI;
import static org.jetbrains.plugins.groovy.lang.lexer.TokenSets.WHITE_SPACES_OR_COMMENTS;
public class GrUnnecessarySemicolonInspection extends GroovySuppressableInspectionTool implements CleanupLocalInspectionTool {
private static final TokenSet NLS_SET = TokenSet.create(mNLS);
private static final TokenSet FORWARD_SET = andNot(orSet(WHITE_SPACES_OR_COMMENTS, TokenSet.create(mSEMI)), NLS_SET);
private static final TokenSet BACKWARD_SET = andNot(WHITE_SPACES_OR_COMMENTS, NLS_SET);
private static final LocalQuickFix FIX = new LocalQuickFix() {
@Nls
@NotNull
@Override
public String getFamilyName() {
return "Remove semicolon";
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiElement element = descriptor.getPsiElement();
if (element != null && element.getNode().getElementType() == mSEMI) {
CodeStyleManager.getInstance(project).performActionWithFormatterDisabled((Runnable)element::delete);
}
}
};
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new PsiElementVisitor() {
@Override
public void visitElement(PsiElement element) {
if (element.getNode().getElementType() != mSEMI) return;
if (isSemicolonNecessary(element)) return;
holder.registerProblem(element, "Semicolon is unnecessary", ProblemHighlightType.LIKE_UNUSED_SYMBOL, FIX);
}
};
}
private static boolean isSemicolonNecessary(@NotNull PsiElement semicolon) {
if (semicolon.getParent() instanceof GrTraditionalForClause) return true;
PsiElement prevSibling = PsiUtil.skipSet(semicolon, false, BACKWARD_SET);
PsiElement nextSibling = PsiUtil.skipSet(semicolon, true, FORWARD_SET);
return prevSibling instanceof GrStatement && (
nextSibling instanceof GrStatement || nextSibling != null && nextSibling.getNextSibling() instanceof GrClosableBlock
);
}
}
@@ -0,0 +1,77 @@
// 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.
package org.jetbrains.plugins.groovy.codeInspection.style
import com.intellij.codeInspection.CleanupLocalInspectionTool
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import org.jetbrains.plugins.groovy.codeInspection.GroovyInspectionBundle.message
import org.jetbrains.plugins.groovy.codeInspection.GroovySuppressableInspectionTool
import org.jetbrains.plugins.groovy.codeInspection.fixes.RemoveElementWithoutFormatterFix
import org.jetbrains.plugins.groovy.lang.lexer.TokenSets.WHITE_SPACES_OR_COMMENTS
import org.jetbrains.plugins.groovy.lang.psi.GroovyElementTypes.*
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrStatement
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock
import org.jetbrains.plugins.groovy.lang.psi.api.statements.clauses.GrTraditionalForClause
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameterList
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrEnumConstantList
import org.jetbrains.plugins.groovy.lang.psi.api.toplevel.imports.GrImportStatement
import org.jetbrains.plugins.groovy.lang.psi.api.toplevel.packaging.GrPackageDefinition
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil.skipSet
import org.jetbrains.plugins.groovy.util.TokenSet
import org.jetbrains.plugins.groovy.util.minus
import org.jetbrains.plugins.groovy.util.plus
class GrUnnecessarySemicolonInspection : GroovySuppressableInspectionTool(), CleanupLocalInspectionTool {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor = object : PsiElementVisitor() {
override fun visitElement(element: PsiElement) {
if (element.node.elementType !== T_SEMI || isSemicolonNecessary(element)) return
holder.registerProblem(
element,
message("unnecessary.semicolon.description"),
ProblemHighlightType.LIKE_UNUSED_SYMBOL,
fix
)
}
}
companion object {
private val fix = RemoveElementWithoutFormatterFix(message("unnecessary.semicolon.fix"))
private val nlSet = TokenSet(NL)
private val forwardSet = WHITE_SPACES_OR_COMMENTS + TokenSet(T_SEMI) - nlSet
private val backwardSet = WHITE_SPACES_OR_COMMENTS - nlSet
private val separators = TokenSet(NL, T_SEMI)
private val previousSet = TokenSet(T_LBRACE, T_ARR)
private fun isSemicolonNecessary(semicolon: PsiElement): Boolean {
if (semicolon.parent is GrTraditionalForClause) return true
val prevSibling = skipSet(semicolon, false, backwardSet) ?: return false
val nextSibling = skipSet(semicolon, true, forwardSet) ?: return false
val prevType = prevSibling.node.elementType
return when {
prevType in separators -> {
prevSibling.prevSibling is GrEnumConstantList
}
prevType in previousSet -> {
false
}
prevSibling is GrStatement -> {
nextSibling is GrStatement || nextSibling.nextSibling is GrClosableBlock
}
prevSibling is GrPackageDefinition || prevSibling is GrImportStatement -> {
nextSibling.node.elementType !in separators
}
prevSibling is GrParameterList && prevSibling.parent is GrClosableBlock -> {
false // beginning of a closure
}
else -> true
}
}
}
}
@@ -0,0 +1,14 @@
// 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.
package org.jetbrains.plugins.groovy.util
import com.intellij.psi.tree.IElementType
import com.intellij.psi.tree.TokenSet
import com.intellij.psi.tree.TokenSet.andNot
import com.intellij.psi.tree.TokenSet.orSet
operator fun TokenSet.plus(another: TokenSet): TokenSet = orSet(this, another)
operator fun TokenSet.minus(another: TokenSet): TokenSet = andNot(this, another)
@Suppress("FunctionName")
fun TokenSet(vararg tokens: IElementType): TokenSet = TokenSet.create(*tokens)
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2016 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.
*/
// 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.
package org.jetbrains.plugins.groovy.codeInspection.style
import com.intellij.testFramework.LightProjectDescriptor
@@ -91,7 +77,7 @@ print(a); /*asd*/
doTest 'foo {; ;1;;/*asd*/;54; ;<caret>;; }', 'foo { 1;/*asd*/54 }'
}
void 'test within closure with arrow' () {
void 'test within closure with arrow'() {
doTest 'foo { -> ; ;1;;/*asd*/;54; ;<caret>;; }', 'foo { -> 1;/*asd*/54 }'
}
@@ -111,6 +97,47 @@ class A {
'''
}
void 'test after package definition'() {
doTest 'package com.foo<caret>; import java.lang.String'
doTest '''\
package com.foo<caret>;
import java.lang.String
''', '''\
package com.foo
import java.lang.String
'''
}
void 'test after import definition'() {
doTest 'import java.lang.String<caret>; import java.lang.String'
doTest '''\
import java.lang.String<caret>;
import java.lang.String;
''', '''\
import java.lang.String
import java.lang.String
'''
}
void 'test after enum constants'() {
doTest '''\
enum E {
foo, bar, <caret>;
E() {}
}
'''
doTest '''\
enum E {
foo, bar,
<caret>;
E() {}
}
'''
}
private doTest(String before, String after) {
fixture.with {
enableInspections GrUnnecessarySemicolonInspection
@@ -124,7 +151,7 @@ class A {
fixture.with {
enableInspections GrUnnecessarySemicolonInspection
configureByText '_.groovy', text
checkHighlighting()
assertNull(getAvailableIntention("Fix all 'Unnecessary semicolon'"))
}
}