Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Features/#148 multiline function call in a single-line declared section #392

Merged
merged 4 commits into from
Jul 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
iromeo marked this conversation as resolved.
Show resolved Hide resolved

invalidWhitespaces.forEach {
registerProblem(
it,
SnakemakeBundle.message("INSP.NAME.multiline.func.call"),
iromeo marked this conversation as resolved.
Show resolved Hide resolved
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 |