diff --git a/CHANGELOG.md b/CHANGELOG.md index 068825ee4..125488d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Released on ... - TODO (see [#NNN](https://github.com/JetBrains-Research/snakecharm/issues/NNN)) ### Added +- Inspection for improperly called functions (see [#148](https://github.com/JetBrains-Research/snakecharm/issues/148)) - Ability for memorising new section name (see [#372](https://github.com/JetBrains-Research/snakecharm/issues/372)) - Support for 'handover' section (see [#362](https://github.com/JetBrains-Research/snakecharm/issues/362)) - Support for 'containerized' section (see [#361](https://github.com/JetBrains-Research/snakecharm/issues/361)) diff --git a/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkMultilineFunctionCallInspection.kt b/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkMultilineFunctionCallInspection.kt new file mode 100644 index 000000000..bc60b73b5 --- /dev/null +++ b/src/main/kotlin/com/jetbrains/snakecharm/inspections/SmkMultilineFunctionCallInspection.kt @@ -0,0 +1,99 @@ +package com.jetbrains.snakecharm.inspections + +import com.intellij.codeInspection.LocalInspectionToolSession +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.* +import com.intellij.psi.util.elementType +import com.intellij.refactoring.suggested.startOffset +import com.jetbrains.python.PyTokenTypes +import com.jetbrains.python.psi.PyCallExpression +import com.jetbrains.snakecharm.SnakemakeBundle +import com.jetbrains.snakecharm.lang.psi.SmkRuleOrCheckpointArgsSection + +class SmkMultilineFunctionCallInspection : SnakemakeInspection() { + override fun buildVisitor( + holder: ProblemsHolder, + isOnTheFly: Boolean, + session: LocalInspectionToolSession + ) = object : SnakemakeInspectionVisitor(holder, session) { + override fun visitSmkRuleOrCheckpointArgsSection(st: SmkRuleOrCheckpointArgsSection) { + val args = st.argumentList ?: return + + if (st.multilineSectionDefinition()) { + return + } + + val invalidWhitespaces = mutableListOf() + args.arguments.forEach { psi -> + if (psi is PyCallExpression) { + collectNewlinesInMultilineCall(psi, invalidWhitespaces) + } + } + + invalidWhitespaces.forEach { + registerProblem( + it, + SnakemakeBundle.message("INSP.NAME.multiline.func.call"), + ShiftToNextLine(st, invalidWhitespaces) + ) + } + } + } + + /** + * Checks whether there are any whitespaces in [expression] argument list, + * if so, checks if it contains new line character, + * if so, adds whitespace nodes to [incorrectElements] + */ + private fun collectNewlinesInMultilineCall( + expression: PyCallExpression, + incorrectElements: MutableList + ) { + var element = expression.argumentList?.firstChild + while (element != null) { + if (element.elementType == TokenType.WHITE_SPACE && element.text.contains('\n')) { + incorrectElements.add(element) + break + } + element = element.nextSibling + } + } + + private class ShiftToNextLine(expr: PsiElement, val incorrectElements: MutableList) : + LocalQuickFixOnPsiElement(expr) { + + override fun getFamilyName() = SnakemakeBundle.message("INSP.NAME.multiline.func.call.fix") + + override fun getText() = familyName + + override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { + val doc = PsiDocumentManager.getInstance(project).getDocument(file) + val indentCandidate = startElement.prevSibling + if (indentCandidate !is PsiWhiteSpace || doc == null) { + return + } + val argumentList = (startElement as SmkRuleOrCheckpointArgsSection).argumentList ?: return + val indent = indentCandidate.text.replace("\n", "") + // Moves every argument list element to new line + argumentList.arguments.forEach { expression -> + doc.insertString(expression.startOffset, "\n$indent$indent") + PsiDocumentManager.getInstance(project).commitDocument(doc) + } + // Deletes every incorrect whitespace + // If there are END_OF_LINE_COMMENT, new whitespace will be inserted automatically + // Otherwise, we need to insert it manually + incorrectElements.forEach { space -> + val hasComment = space.prevSibling.elementType == PyTokenTypes.END_OF_LINE_COMMENT + val offset = space.startOffset + space.delete() + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(doc) + if (!hasComment) { + doc.insertString(offset, "\n$indent$indent$indent") + PsiDocumentManager.getInstance(project).commitDocument(doc) + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkPsiElements.kt b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkPsiElements.kt index acd46d700..30d04f2a8 100644 --- a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkPsiElements.kt +++ b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/SmkPsiElements.kt @@ -33,6 +33,11 @@ interface SmkRuleOrCheckpointArgsSection : SmkArgsSection, PyTypedElement { // P fun isWildcardsDefiningSection() = sectionKeyword in WILDCARDS_DEFINING_SECTIONS_KEYWORDS override fun getParentRuleOrCheckPoint(): SmkRuleOrCheckpoint = super.getParentRuleOrCheckPoint()!! + + /** + * Checks if section argument list starts from new line + */ + fun multilineSectionDefinition(): Boolean } interface SmkSubworkflowArgsSection : SmkArgsSection { diff --git a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/impl/SmkRuleOrCheckpointArgsSectionImpl.kt b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/impl/SmkRuleOrCheckpointArgsSectionImpl.kt index 5569bca39..810ab0679 100644 --- a/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/impl/SmkRuleOrCheckpointArgsSectionImpl.kt +++ b/src/main/kotlin/com/jetbrains/snakecharm/lang/psi/impl/SmkRuleOrCheckpointArgsSectionImpl.kt @@ -3,6 +3,8 @@ package com.jetbrains.snakecharm.lang.psi.impl import com.intellij.lang.ASTNode import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.psi.PsiReference +import com.intellij.psi.TokenType +import com.jetbrains.python.PyTokenTypes import com.jetbrains.python.psi.PyElementVisitor import com.jetbrains.python.psi.PyStringLiteralExpression import com.jetbrains.python.psi.types.TypeEvalContext @@ -10,11 +12,9 @@ import com.jetbrains.snakecharm.lang.SnakemakeNames import com.jetbrains.snakecharm.lang.psi.* import com.jetbrains.snakecharm.lang.psi.types.SmkRuleLikeSectionArgsType -open class SmkRuleOrCheckpointArgsSectionImpl(node: ASTNode): SmkArgsSectionImpl(node), - SmkRuleOrCheckpointArgsSection -{ - override fun getType(context: TypeEvalContext, key: TypeEvalContext.Key) - = SmkRuleLikeSectionArgsType(this) +open class SmkRuleOrCheckpointArgsSectionImpl(node: ASTNode) : SmkArgsSectionImpl(node), + SmkRuleOrCheckpointArgsSection { + override fun getType(context: TypeEvalContext, key: TypeEvalContext.Key) = SmkRuleLikeSectionArgsType(this) override fun acceptPyVisitor(pyVisitor: PyElementVisitor) = when (pyVisitor) { is SmkElementVisitor -> pyVisitor.visitSmkRuleOrCheckpointArgsSection(this) @@ -42,11 +42,11 @@ open class SmkRuleOrCheckpointArgsSectionImpl(node: ASTNode): SmkArgsSectionImpl private fun getSimplePathRelatedSectionReference( refFun: (PyStringLiteralExpression, Int) -> SmkFileReference - ) : PsiReference? { + ): PsiReference? { val stringLiteral = - argumentList?.arguments - ?.firstOrNull { it is PyStringLiteralExpression } - as? PyStringLiteralExpression ?: return null + argumentList?.arguments + ?.firstOrNull { it is PyStringLiteralExpression } + as? PyStringLiteralExpression ?: return null // No reference if language is injected val languageManager = InjectedLanguageManager.getInstance(project) @@ -57,4 +57,24 @@ open class SmkRuleOrCheckpointArgsSectionImpl(node: ASTNode): SmkArgsSectionImpl val offsetInParent = sectionKeyword!!.length + stringLiteral.startOffsetInParent return refFun(stringLiteral, offsetInParent) } + + override fun multilineSectionDefinition(): Boolean = multilineSectionDefinition(this) + + companion object { + fun multilineSectionDefinition(argsSection: SmkArgsSection): Boolean { + var node = argsSection.argumentList?.node?.findChildByType(PyTokenTypes.COLON)?.treeNext ?: return false + while (true) { + node = when (node.elementType) { + TokenType.WHITE_SPACE -> { + if (node.text.contains('\n')) { + return true + } + node.treeNext + } + PyTokenTypes.END_OF_LINE_COMMENT -> node.treeNext + else -> return false + } + } + } + } } \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 1d93070ca..c7d216d26 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -533,6 +533,17 @@ implementationClass="com.jetbrains.snakecharm.inspections.SmkMisuseUsageIOFlagMethodsInspection" /> + + + +Checks whether there are function call which spans over multiple lines but starts at the same line as section. + + \ No newline at end of file diff --git a/src/test/resources/features/highlighting/inspections/multiline_function_call_inspection.feature b/src/test/resources/features/highlighting/inspections/multiline_function_call_inspection.feature new file mode 100644 index 000000000..c3a050ca7 --- /dev/null +++ b/src/test/resources/features/highlighting/inspections/multiline_function_call_inspection.feature @@ -0,0 +1,83 @@ +Feature: Inspection for multiline function calls in sections, which were declared in a single line style + + Scenario Outline: No inspection in multiline declared section + Given a snakemake project + Given I open a file "foo.smk" with text + """ + NAME: + input: + foo("abc","abcde"), + foo2("text1", + "text2","text3"), + foo3("text4","text2", + "text3") + rule_148_c: + input: #ffff + foo1("text1", + "text2", "text3") + """ + And SmkMultilineFunctionCallInspection inspection is enabled + Then I expect no inspection errors + When I check highlighting errors + Examples: + | rule_like | + | rule | + | checkpoint | + + Scenario Outline: Inspection in single line declared section + Given a snakemake project + Given I open a file "foo.smk" with text + """ + NAME: + input: foo("abc","abcde"), foo2("text1", + "text2","text3"), foo3("text4","text2", + "text3") + """ + And SmkMultilineFunctionCallInspection inspection is enabled + Then I expect inspection error on pattern <\n > with message + """ + Invalid function call. Rewrite section as multiline or rewrite function using a single line + """ + Then I expect inspection error on pattern <\n > with message + """ + Invalid function call. Rewrite section as multiline or rewrite function using a single line + """ + When I check highlighting errors + Examples: + | rule_like | + | rule | + | checkpoint | + + Scenario Outline: Quickfix for inspection in single line declared section + Given a snakemake project + Given I open a file "foo.smk" with text + """ + NAME: + input:foo("abc","abcde"),foo2("text1", #comments here + "text2","text3"),foo3("text4","text2", + "text3") + """ + And SmkMultilineFunctionCallInspection inspection is enabled + Then I expect inspection error on pattern <\n > with message + """ + Invalid function call. Rewrite section as multiline or rewrite function using a single line + """ + Then I expect inspection error on pattern <\n > with message + """ + Invalid function call. Rewrite section as multiline or rewrite function using a single line + """ + When I check highlighting errors + Then I invoke quick fix Rewrite section as multiline and see text: + """ + NAME: + input: + foo("abc","abcde"), + foo2("text1", #comments here + "text2","text3"), + foo3("text4","text2", + "text3") + """ + Examples: + | rule_like | + | rule | + | checkpoint | \ No newline at end of file