Skip to content

Commit

Permalink
Features/#148 multiline function call in a single-line declared secti…
Browse files Browse the repository at this point in the history
…on (#392)

feat: inspection for improperly called function is added, see #148

Resolves: #148
  • Loading branch information
dakochik authored Jul 29, 2021
1 parent b3588f2 commit de8adcd
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PsiElement>()
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<PsiElement>
) {
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<PsiElement>) :
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)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ 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
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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
}
}
}
}
11 changes: 11 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,17 @@
implementationClass="com.jetbrains.snakecharm.inspections.SmkMisuseUsageIOFlagMethodsInspection"
/>

<localInspection
language="Snakemake" shortName="SmkMultilineFunctionCallInspection"
enabledByDefault="true"
level="ERROR"
suppressId="SmkMultilineFunctionCall"
bundle="SnakemakeBundle"
groupKey="INSP.GROUP.snakemake"
key="INSP.NAME.multiline.func.call"
implementationClass="com.jetbrains.snakecharm.inspections.SmkMultilineFunctionCallInspection"
/>

<findUsagesHandlerFactory
implementation="com.jetbrains.snakecharm.codeInsight.refactoring.SmkFindUsagesHandlerFactory"
id="Python" order="last, before default"
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/SnakemakeBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ INSP.NAME.redundant.comma.fix.message=Remove redundant comma
INSP.NAME.misuse.usage.io.flag.methods.title=Correct using methods ancient, protected, directory
INSP.NAME.misuse.usage.io.flag.methods.warning.message=''{0}'' isn''t supported in ''{1}'' section, expected in sections: {2}.

# SmkMultilineFunctionCallInspection
INSP.NAME.multiline.func.call=Invalid function call. Rewrite section as multiline or rewrite function using a single line
INSP.NAME.multiline.func.call.fix= Rewrite section as multiline

# SmkPyUnboundLocalVariableInspection
INSP.NAME.unbound=Unbound local variable

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<html lang="Snakemake">
<body>
Checks whether there are function call which spans over multiple lines but starts at the same line as section.
</body>
</html>
Original file line number Diff line number Diff line change
@@ -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
"""
<rule_like> NAME:
input:
foo("abc","abcde"),
foo2("text1",
"text2","text3"),
foo3("text4","text2",
"text3")
<rule_like> 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
"""
<rule_like> 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
"""
<rule_like> 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:
"""
<rule_like> NAME:
input:
foo("abc","abcde"),
foo2("text1", #comments here
"text2","text3"),
foo3("text4","text2",
"text3")
"""
Examples:
| rule_like |
| rule |
| checkpoint |

0 comments on commit de8adcd

Please sign in to comment.