diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/GroovyInspectionBundle.properties b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/GroovyInspectionBundle.properties index 53e5fd1496a8..f94203d332f1 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/GroovyInspectionBundle.properties +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/GroovyInspectionBundle.properties @@ -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 \ No newline at end of file +unnecessary.alias.description=Alias is unnecessary +unnecessary.semicolon.description=Semicolon is unnecessary +unnecessary.semicolon.fix=Remove semicolon diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/fixes/RemoveElementWithoutFormatterFix.kt b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/fixes/RemoveElementWithoutFormatterFix.kt new file mode 100644 index 000000000000..aacd3769fce9 --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/fixes/RemoveElementWithoutFormatterFix.kt @@ -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() + } + } +} diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/style/GrUnnecessarySemicolonInspection.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/style/GrUnnecessarySemicolonInspection.java deleted file mode 100644 index 4e0cd22042f7..000000000000 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/style/GrUnnecessarySemicolonInspection.java +++ /dev/null @@ -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 - ); - } -} diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/style/GrUnnecessarySemicolonInspection.kt b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/style/GrUnnecessarySemicolonInspection.kt new file mode 100644 index 000000000000..b6ad26027591 --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/style/GrUnnecessarySemicolonInspection.kt @@ -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 + } + } + } +} diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/util/tokenSet.kt b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/util/tokenSet.kt new file mode 100644 index 000000000000..40114903443c --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/util/tokenSet.kt @@ -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) diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/codeInspection/style/GrUnnecessarySemicolonInspectionTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/codeInspection/style/GrUnnecessarySemicolonInspectionTest.groovy index 50a75dec839f..d562d1c03afb 100644 --- a/plugins/groovy/test/org/jetbrains/plugins/groovy/codeInspection/style/GrUnnecessarySemicolonInspectionTest.groovy +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/codeInspection/style/GrUnnecessarySemicolonInspectionTest.groovy @@ -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; ;;; }', 'foo { 1;/*asd*/54 }' } - void 'test within closure with arrow' () { + void 'test within closure with arrow'() { doTest 'foo { -> ; ;1;;/*asd*/;54; ;;; }', 'foo { -> 1;/*asd*/54 }' } @@ -111,6 +97,47 @@ class A { ''' } + void 'test after package definition'() { + doTest 'package com.foo; import java.lang.String' + doTest '''\ +package com.foo; +import java.lang.String +''', '''\ +package com.foo +import java.lang.String +''' + } + + void 'test after import definition'() { + doTest 'import java.lang.String; import java.lang.String' + doTest '''\ +import java.lang.String; +import java.lang.String; +''', '''\ +import java.lang.String +import java.lang.String +''' + } + + void 'test after enum constants'() { + doTest '''\ +enum E { + foo, bar, ; + E() {} +} +''' + doTest '''\ +enum E { + foo, bar, + + ; + + + 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'")) } }