diff --git a/src/main/kotlin/org/move/ide/formatter/impl/spacing.kt b/src/main/kotlin/org/move/ide/formatter/impl/spacing.kt index 27f0005a3..dabfcff9e 100644 --- a/src/main/kotlin/org/move/ide/formatter/impl/spacing.kt +++ b/src/main/kotlin/org/move/ide/formatter/impl/spacing.kt @@ -14,7 +14,7 @@ import com.intellij.psi.tree.IElementType import com.intellij.psi.tree.TokenSet import org.move.ide.formatter.MvFmtContext import org.move.lang.MvElementTypes.* -import org.move.lang.core.MOVE_COMMENTS +import org.move.lang.core.MV_COMMENTS import org.move.lang.core.MOVE_KEYWORDS import org.move.lang.core.psi.MvAddressBlock import org.move.lang.core.psi.MvModule @@ -219,7 +219,7 @@ private fun ASTNode?.isWhiteSpaceWithLineBreak(): Boolean = this != null && elementType == TokenType.WHITE_SPACE && textContains('\n') private fun SpacingContext.needsBlankLineBetweenItems(): Boolean { - if (elementType1 in MOVE_COMMENTS || elementType2 in MOVE_COMMENTS) + if (elementType1 in MV_COMMENTS || elementType2 in MV_COMMENTS) return false // Allow to keep consecutive runs of `use`, `const` or other "one line" items without blank lines diff --git a/src/main/kotlin/org/move/ide/inspections/MvInspectionSuppressor.kt b/src/main/kotlin/org/move/ide/inspections/MvInspectionSuppressor.kt new file mode 100644 index 000000000..8c66121f1 --- /dev/null +++ b/src/main/kotlin/org/move/ide/inspections/MvInspectionSuppressor.kt @@ -0,0 +1,98 @@ +package org.move.ide.inspections + +import com.intellij.codeInspection.* +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.codeInspection.util.IntentionName +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Iconable +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiWhiteSpace +import org.move.lang.core.MV_COMMENTS +import org.move.lang.core.psi.MvAttr +import org.move.lang.core.psi.MvFunction +import org.move.lang.core.psi.MvModule +import org.move.lang.core.psi.ext.* +import org.move.lang.core.psi.psiFactory +import javax.swing.Icon + +class MvInspectionSuppressor: InspectionSuppressor { + override fun getSuppressActions(element: PsiElement?, toolId: String): Array { + val ancestors = element?.ancestors.orEmpty().filterIsInstance() + // todo: add suppression with comments for other inspections + if (toolId !in APTOS_LINTS) return emptyArray() + return ancestors.mapNotNull { + when (it) { + is MvFunction -> { + SuppressInspectionWithAttributeFix(it, toolId, "function") + } + is MvModule -> SuppressInspectionWithAttributeFix(it, toolId, "module") + else -> null + } + }.toList().toTypedArray() + } + + override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean { + return element.ancestors.filterIsInstance() + .any { + isSuppressedByComment(it, toolId) + || isSuppressedByAttribute(it, toolId) + } + } + + private fun isSuppressedByComment(element: MvDocAndAttributeOwner, toolId: String): Boolean { + return element.leadingComments().any { comment -> + val matcher = SuppressionUtil.SUPPRESS_IN_LINE_COMMENT_PATTERN.matcher(comment.text) + matcher.matches() && SuppressionUtil.isInspectionToolIdMentioned(matcher.group(1), toolId) + } + } + + // special alternativeId for some inspection that could be suppressed by the attributes, in form of + // lint::LINT_NAME, and it's suppressable by the #[lint::skip(LINT_NAME)] + private fun isSuppressedByAttribute(element: MvDocAndAttributeOwner, toolId: String): Boolean { + if (toolId !in APTOS_LINTS) return false + return element.queryAttributes + .getAttrItemsByPath("lint::skip") + .any { it.innerAttrItems.any { it.textMatches(toolId) } } + } + + @Suppress("PrivatePropertyName") + private val APTOS_LINTS = setOf(MvNeedlessDerefRefInspection.LINT_ID) +} + +private class SuppressInspectionWithAttributeFix( + item: MvDocAndAttributeOwner, + val toolId: String, + val itemId: String, +): LocalQuickFixOnPsiElement(item), ContainerBasedSuppressQuickFix, Iconable { + + override fun getFamilyName(): @IntentionFamilyName String = "Suppress '$toolId' inspections" + override fun getText(): @IntentionName String = "Suppress with '#[lint::skip($toolId)]' for $itemId" + + override fun isSuppressAll(): Boolean = false + override fun getIcon(flags: Int): Icon? = AllIcons.Ide.HectorOff + + override fun getContainer(context: PsiElement?): PsiElement? = this.startElement + + override fun isAvailable( + project: Project, + context: PsiElement + ): Boolean = context.isValid && getContainer(context) != null + + override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { + val element = startElement as? MvDocAndAttributeOwner ?: return + val skipAttribute = project.psiFactory.attribute("#[lint::skip($toolId)]") + val anchor = element.childrenWithLeaves + .dropWhile { it is PsiComment || it is PsiWhiteSpace || it is MvAttr }.firstOrNull() + val addedAttribute = element.addBefore(skipAttribute, anchor) + element.addAfter(project.psiFactory.newline(), addedAttribute) + } +} + +private fun MvDocAndAttributeOwner.leadingComments(): Sequence = + generateSequence(firstChild) { psi -> + psi.nextSibling.takeIf { it.elementType in MV_COMMENTS || it is PsiWhiteSpace } + } + .filterIsInstance() diff --git a/src/main/kotlin/org/move/ide/inspections/MvLocalInspectionTool.kt b/src/main/kotlin/org/move/ide/inspections/MvLocalInspectionTool.kt index fe33911db..699f49d23 100644 --- a/src/main/kotlin/org/move/ide/inspections/MvLocalInspectionTool.kt +++ b/src/main/kotlin/org/move/ide/inspections/MvLocalInspectionTool.kt @@ -50,7 +50,7 @@ abstract class MvLocalInspectionTool : LocalInspectionTool() { abstract class DiagnosticFix(element: T) : LocalQuickFixOnPsiElement(element) { - val targetElement: T? get() = this.startElement + protected val targetElement: T? get() = this.startElement override fun getStartElement(): T? { @Suppress("UNCHECKED_CAST") diff --git a/src/main/kotlin/org/move/ide/inspections/MvRedundantRefDerefInspection.kt b/src/main/kotlin/org/move/ide/inspections/MvNeedlessDerefRefInspection.kt similarity index 86% rename from src/main/kotlin/org/move/ide/inspections/MvRedundantRefDerefInspection.kt rename to src/main/kotlin/org/move/ide/inspections/MvNeedlessDerefRefInspection.kt index f29d8fd21..8ff878593 100644 --- a/src/main/kotlin/org/move/ide/inspections/MvRedundantRefDerefInspection.kt +++ b/src/main/kotlin/org/move/ide/inspections/MvNeedlessDerefRefInspection.kt @@ -4,6 +4,7 @@ import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile +import org.jetbrains.annotations.NonNls import org.move.lang.core.psi.MvBorrowExpr import org.move.lang.core.psi.MvDerefExpr import org.move.lang.core.psi.MvExpr @@ -11,13 +12,16 @@ import org.move.lang.core.psi.MvParensExpr import org.move.lang.core.psi.MvVisitor import org.move.lang.core.psi.ext.unwrap -class MvRedundantRefDerefInspection: MvLocalInspectionTool() { +class MvNeedlessDerefRefInspection: MvLocalInspectionTool() { + override fun getID(): @NonNls String = LINT_ID + override fun buildMvVisitor( holder: ProblemsHolder, isOnTheFly: Boolean ): MvVisitor = object: MvVisitor() { override fun visitDerefExpr(o: MvDerefExpr) { + val a = 1 val innerExpr = o.innerExpr if (innerExpr is MvBorrowExpr) { if (innerExpr.expr == null) return @@ -41,6 +45,10 @@ class MvRedundantRefDerefInspection: MvLocalInspectionTool() { element.replace(itemExpr) } } + + companion object { + const val LINT_ID = "needless_deref_ref" + } } private val MvDerefExpr.innerExpr: MvExpr? get() { diff --git a/src/main/kotlin/org/move/ide/refactoring/MvImportOptimizer.kt b/src/main/kotlin/org/move/ide/refactoring/MvImportOptimizer.kt index 6f077de80..1af18e8a5 100644 --- a/src/main/kotlin/org/move/ide/refactoring/MvImportOptimizer.kt +++ b/src/main/kotlin/org/move/ide/refactoring/MvImportOptimizer.kt @@ -105,11 +105,11 @@ class MvImportOptimizer : ImportOptimizer { .sorted() for ((useWrapper, nextUseWrapper) in sortedUses.withNext()) { val addedUseItem = itemsOwner.addBefore(useWrapper.useStmt, firstItem) - itemsOwner.addAfter(psiFactory.createNewline(), addedUseItem) + itemsOwner.addAfter(psiFactory.newline(), addedUseItem) val addNewLine = useWrapper.packageGroupLevel != nextUseWrapper?.packageGroupLevel if (addNewLine) { - itemsOwner.addAfter(psiFactory.createNewline(), addedUseItem) + itemsOwner.addAfter(psiFactory.newline(), addedUseItem) } } useStmts.forEach { diff --git a/src/main/kotlin/org/move/ide/search/MvWordsScanner.kt b/src/main/kotlin/org/move/ide/search/MvWordsScanner.kt index ad7df4a18..21a119103 100644 --- a/src/main/kotlin/org/move/ide/search/MvWordsScanner.kt +++ b/src/main/kotlin/org/move/ide/search/MvWordsScanner.kt @@ -3,13 +3,13 @@ package org.move.ide.search import com.intellij.lang.cacheBuilder.DefaultWordsScanner import org.move.lang.MvElementTypes.BYTE_STRING_LITERAL import org.move.lang.MvElementTypes.IDENTIFIER -import org.move.lang.core.MOVE_COMMENTS +import org.move.lang.core.MV_COMMENTS import org.move.lang.core.lexer.createMoveLexer import org.move.lang.core.tokenSetOf class MvWordsScanner : DefaultWordsScanner( createMoveLexer(), tokenSetOf(IDENTIFIER), - MOVE_COMMENTS, + MV_COMMENTS, tokenSetOf(BYTE_STRING_LITERAL) ) diff --git a/src/main/kotlin/org/move/ide/typing/MvBraceMatcher.kt b/src/main/kotlin/org/move/ide/typing/MvBraceMatcher.kt index 44a0b9be3..da9137425 100644 --- a/src/main/kotlin/org/move/ide/typing/MvBraceMatcher.kt +++ b/src/main/kotlin/org/move/ide/typing/MvBraceMatcher.kt @@ -12,7 +12,7 @@ import com.intellij.psi.tree.TokenSet import org.move.lang.MoveFileType import org.move.lang.MoveLanguage import org.move.lang.MvElementTypes.* -import org.move.lang.core.MOVE_COMMENTS +import org.move.lang.core.MV_COMMENTS import java.util.* private class MvPairedBraceMatcher : PairedBraceMatcher { @@ -32,7 +32,7 @@ private class MvPairedBraceMatcher : PairedBraceMatcher { ) private val InsertPairBraceBefore = TokenSet.orSet( - MOVE_COMMENTS, + MV_COMMENTS, TokenSet.create( TokenType.WHITE_SPACE, SEMICOLON, @@ -144,7 +144,7 @@ class MvBraceMatcher : PairedBraceMatcherAdapter(MvPairedBraceMatcher(), MoveLan private val OPEN_BRACES = TokenSet.create(LT, L_PAREN, L_BRACE, L_BRACK) val TYPE_PARAMETER_TOKENS = TokenSet.orSet( - MOVE_COMMENTS, + MV_COMMENTS, TokenSet.create( TokenType.WHITE_SPACE, IDENTIFIER, diff --git a/src/main/kotlin/org/move/ide/utils/imports/ImportUtils.kt b/src/main/kotlin/org/move/ide/utils/imports/ImportUtils.kt index 251269185..7f3de3e7d 100644 --- a/src/main/kotlin/org/move/ide/utils/imports/ImportUtils.kt +++ b/src/main/kotlin/org/move/ide/utils/imports/ImportUtils.kt @@ -1,6 +1,5 @@ package org.move.ide.utils.imports -import com.intellij.util.concurrency.annotations.RequiresWriteLock import org.move.ide.inspections.imports.usageScope import org.move.lang.core.psi.* import org.move.lang.core.psi.ext.* @@ -159,7 +158,7 @@ private val List.lastElement: T? get() = maxByOrNull { it.text @Suppress("SameReturnValue") private fun insertUseStmtAtTheCorrectLocation(mod: MvItemsOwner, useStmt: MvUseStmt): Boolean { val psiFactory = MvPsiFactory(mod.project) - val newline = psiFactory.createNewline() + val newline = psiFactory.newline() val useStmts = mod.childrenOfType().map(::UseStmtWrapper) if (useStmts.isEmpty()) { val anchor = mod.firstItem diff --git a/src/main/kotlin/org/move/lang/core/MoveParserUtil.kt b/src/main/kotlin/org/move/lang/core/MoveParserUtil.kt index f571bbaf5..4e72794f5 100644 --- a/src/main/kotlin/org/move/lang/core/MoveParserUtil.kt +++ b/src/main/kotlin/org/move/lang/core/MoveParserUtil.kt @@ -163,7 +163,7 @@ object MoveParserUtil: GeneratedParserUtilBase() { @JvmStatic private fun consumeStopOnWhitespaceOrComment(b: PsiBuilder, tokens: TokenSet): Boolean { val nextTokenType = b.rawLookup(1) - if (nextTokenType in MOVE_COMMENTS || nextTokenType == WHITE_SPACE) { + if (nextTokenType in MV_COMMENTS || nextTokenType == WHITE_SPACE) { consumeToken(b, tokens) return false } diff --git a/src/main/kotlin/org/move/lang/core/MvTokenType.kt b/src/main/kotlin/org/move/lang/core/MvTokenType.kt index 1b1c33c57..5edb10533 100644 --- a/src/main/kotlin/org/move/lang/core/MvTokenType.kt +++ b/src/main/kotlin/org/move/lang/core/MvTokenType.kt @@ -29,7 +29,7 @@ val MOVE_KEYWORDS = TokenSet.orSet( val FUNCTION_MODIFIERS = tokenSetOf(VISIBILITY_MODIFIER, NATIVE, ENTRY, INLINE) val TYPES = tokenSetOf(PATH_TYPE, REF_TYPE, TUPLE_TYPE) -val MOVE_COMMENTS = tokenSetOf(BLOCK_COMMENT, EOL_COMMENT, EOL_DOC_COMMENT) +val MV_COMMENTS = tokenSetOf(BLOCK_COMMENT, EOL_COMMENT, EOL_DOC_COMMENT) val MOVE_ARITHMETIC_BINARY_OPS = tokenSetOf( PLUS, MINUS, MUL, DIV, MODULO, diff --git a/src/main/kotlin/org/move/lang/core/psi/MvPsiFactory.kt b/src/main/kotlin/org/move/lang/core/psi/MvPsiFactory.kt index ce3f8922d..56f1bc706 100644 --- a/src/main/kotlin/org/move/lang/core/psi/MvPsiFactory.kt +++ b/src/main/kotlin/org/move/lang/core/psi/MvPsiFactory.kt @@ -176,6 +176,11 @@ class MvPsiFactory(val project: Project) { ?: error("`$text`") } + fun attribute(attrText: String): MvAttr { + return createFromText("$attrText module 0x0::_DummyModule {} ") + ?: error("Invalid attribute `$attrText`") + } + fun function(text: String, moduleName: String = "_Dummy"): MvFunction = createFromText("module $moduleName { $text } ") ?: error("Failed to create a function from text: `$text`") @@ -199,10 +204,9 @@ class MvPsiFactory(val project: Project) { createFromText("module $moduleName { $text } ") ?: error("Failed to create a function from text: `$text`") - fun createWhitespace(ws: String): PsiElement = - PsiParserFacade.getInstance(project).createWhiteSpaceFromText(ws) + fun createWhitespace(ws: String): PsiElement = PsiParserFacade.getInstance(project).createWhiteSpaceFromText(ws) - fun createNewline(): PsiElement = createWhitespace("\n") + fun newline(): PsiElement = createWhitespace("\n") inline fun createFromText(@Language("Move") code: CharSequence): T? { val dummyFile = PsiFileFactory.getInstance(project) diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/MvDocAndAttributeOwner.kt b/src/main/kotlin/org/move/lang/core/psi/ext/MvDocAndAttributeOwner.kt index d9b03ed5f..ec52acdda 100644 --- a/src/main/kotlin/org/move/lang/core/psi/ext/MvDocAndAttributeOwner.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/MvDocAndAttributeOwner.kt @@ -66,6 +66,9 @@ class QueryAttributes( fun getAttrItem(attributeName: String): MvAttrItem? = this.attrItems.find { it.unqualifiedName == attributeName } + fun getAttrItemsByPath(fqPath: String): Sequence = + this.attrItems.filter { it.path.text == fqPath } + val attrItems: Sequence get() = this.attributes.flatMap { it.attrItemList } override fun toString(): String = diff --git a/src/main/kotlin/org/move/lang/core/psi/ext/PsiElement.kt b/src/main/kotlin/org/move/lang/core/psi/ext/PsiElement.kt index 761113db5..a217addd1 100644 --- a/src/main/kotlin/org/move/lang/core/psi/ext/PsiElement.kt +++ b/src/main/kotlin/org/move/lang/core/psi/ext/PsiElement.kt @@ -171,6 +171,9 @@ inline fun PsiElement.ancestorStrict(stopAt: Class PsiElement.ancestorOrSelf(): T? = PsiTreeUtil.getParentOfType(this, T::class.java, false) +inline fun PsiElement.ancestorOrSelf(stopAt: Class): T? = + PsiTreeUtil.getParentOfType(this, T::class.java, false, stopAt) + fun PsiElement.ancestorOfClass(psiClass: Class, strict: Boolean = false): T? = PsiTreeUtil.getParentOfType(this, psiClass, strict) @@ -180,9 +183,6 @@ inline fun PsiElement.hasAncestor(): Boolean = inline fun PsiElement.hasAncestorOrSelf(): Boolean = ancestorOrSelf() != null -inline fun PsiElement.ancestorOrSelf(stopAt: Class): T? = - PsiTreeUtil.getParentOfType(this, T::class.java, false, stopAt) - inline fun PsiElement.stubAncestorStrict(): T? = PsiTreeUtil.getStubOrPsiParentOfType(this, T::class.java) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index be8eaf1ac..58735ddeb 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -232,6 +232,8 @@ + + implementationClass="org.move.ide.inspections.MvNeedlessDerefRefInspection" /> */*caret*/&1; + } + } + """ + ) + + fun `test with comment suppression`() = checkWarnings( + """ + module 0x1::m { + //noinspection needless_deref_ref + fun main() { + */*caret*/&1; + } + } + //noinspection needless_deref_ref + module 0x1::m2 { + fun main() { + */*caret*/&1; + } + } + """ + ) + + fun `test with attribute suppression`() = checkWarnings( + """ + module 0x1::m { + #[lint::skip(needless_deref_ref)] + fun main() { + */*caret*/&1; + } + } + #[lint::skip(needless_deref_ref)] + module 0x1::m2 { + fun main() { + */*caret*/&1; + } + } + """ + ) + + fun `test suppress with attribute fix for function`() = checkFixByText( + "Suppress with '#[lint::skip(needless_deref_ref)]' for function", + """ + module 0x1::m { + fun main() { + */*caret*/&1; + } + } + """, """ + module 0x1::m { + #[lint::skip(needless_deref_ref)] + fun main() { + *&1; + } + } + """, + ) + + fun `test suppress with attribute fix for module`() = checkFixByText( + "Suppress with '#[lint::skip(needless_deref_ref)]' for module", + """ + module 0x1::m { + fun main() { + */*caret*/&1; + } + } + """, """ + #[lint::skip(needless_deref_ref)] + module 0x1::m { + fun main() { + *&1; + } + } + """, + ) +} \ No newline at end of file diff --git a/src/test/kotlin/org/move/ide/inspections/MvRedundantRefDerefInspectionTest.kt b/src/test/kotlin/org/move/ide/inspections/MvNeedlessDerefRefInspectionTest.kt similarity index 96% rename from src/test/kotlin/org/move/ide/inspections/MvRedundantRefDerefInspectionTest.kt rename to src/test/kotlin/org/move/ide/inspections/MvNeedlessDerefRefInspectionTest.kt index 488192e4f..13f5419ff 100644 --- a/src/test/kotlin/org/move/ide/inspections/MvRedundantRefDerefInspectionTest.kt +++ b/src/test/kotlin/org/move/ide/inspections/MvNeedlessDerefRefInspectionTest.kt @@ -3,7 +3,7 @@ package org.move.ide.inspections import org.intellij.lang.annotations.Language import org.move.utils.tests.annotation.InspectionTestBase -class MvRedundantRefDerefInspectionTest: InspectionTestBase(MvRedundantRefDerefInspection::class) { +class MvNeedlessDerefRefInspectionTest: InspectionTestBase(MvNeedlessDerefRefInspection::class) { fun `test no error`() = checkWarnings( """ module 0x1::m {