-
Notifications
You must be signed in to change notification settings - Fork 81
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
Support context receivers #400
Changes from all commits
8993576
14e5f1d
9c3d852
0694e1e
d9aeecd
8502fd5
e2fe9bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,6 +52,7 @@ import org.jetbrains.kotlin.psi.KtCollectionLiteralExpression | |
import org.jetbrains.kotlin.psi.KtConstantExpression | ||
import org.jetbrains.kotlin.psi.KtConstructorDelegationCall | ||
import org.jetbrains.kotlin.psi.KtContainerNode | ||
import org.jetbrains.kotlin.psi.KtContextReceiverList | ||
import org.jetbrains.kotlin.psi.KtContinueExpression | ||
import org.jetbrains.kotlin.psi.KtDelegatedSuperTypeEntry | ||
import org.jetbrains.kotlin.psi.KtDestructuringDeclaration | ||
|
@@ -94,6 +95,7 @@ import org.jetbrains.kotlin.psi.KtQualifiedExpression | |
import org.jetbrains.kotlin.psi.KtReferenceExpression | ||
import org.jetbrains.kotlin.psi.KtReturnExpression | ||
import org.jetbrains.kotlin.psi.KtScript | ||
import org.jetbrains.kotlin.psi.KtScriptInitializer | ||
import org.jetbrains.kotlin.psi.KtSecondaryConstructor | ||
import org.jetbrains.kotlin.psi.KtSimpleNameExpression | ||
import org.jetbrains.kotlin.psi.KtStringTemplateExpression | ||
|
@@ -124,6 +126,7 @@ import org.jetbrains.kotlin.psi.psiUtil.children | |
import org.jetbrains.kotlin.psi.psiUtil.getPrevSiblingIgnoringWhitespace | ||
import org.jetbrains.kotlin.psi.psiUtil.startOffset | ||
import org.jetbrains.kotlin.psi.psiUtil.startsWithComment | ||
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes | ||
|
||
/** An AST visitor that builds a stream of {@link Op}s to format. */ | ||
class KotlinInputAstVisitor( | ||
|
@@ -162,6 +165,7 @@ class KotlinInputAstVisitor( | |
builder.sync(function) | ||
builder.block(ZERO) { | ||
visitFunctionLikeExpression( | ||
function.getStubOrPsiChild(KtStubElementTypes.CONTEXT_RECEIVER_LIST), | ||
function.modifierList, | ||
"fun", | ||
function.typeParameterList, | ||
|
@@ -282,6 +286,7 @@ class KotlinInputAstVisitor( | |
* list of supertypes. | ||
*/ | ||
private fun visitFunctionLikeExpression( | ||
contextReceiverList: KtContextReceiverList?, | ||
modifierList: KtModifierList?, | ||
keyword: String, | ||
typeParameters: KtTypeParameterList?, | ||
|
@@ -294,6 +299,9 @@ class KotlinInputAstVisitor( | |
typeOrDelegationCall: KtElement?, | ||
) { | ||
builder.block(ZERO) { | ||
if (contextReceiverList != null) { | ||
visitContextReceiverList(contextReceiverList) | ||
} | ||
if (modifierList != null) { | ||
visitModifierList(modifierList) | ||
} | ||
|
@@ -1372,6 +1380,7 @@ class KotlinInputAstVisitor( | |
|
||
builder.block(ZERO) { | ||
visitFunctionLikeExpression( | ||
null, | ||
accessor.modifierList, | ||
accessor.namePlaceholder.text, | ||
null, | ||
|
@@ -1461,8 +1470,12 @@ class KotlinInputAstVisitor( | |
|
||
override fun visitClassOrObject(classOrObject: KtClassOrObject) { | ||
builder.sync(classOrObject) | ||
val contextReceiverList = classOrObject.getStubOrPsiChild(KtStubElementTypes.CONTEXT_RECEIVER_LIST) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a method for this: getContextReceiverList() |
||
val modifierList = classOrObject.modifierList | ||
builder.block(ZERO) { | ||
if (contextReceiverList != null) { | ||
visitContextReceiverList(contextReceiverList) | ||
} | ||
if (modifierList != null) { | ||
visitModifierList(modifierList) | ||
} | ||
|
@@ -1533,6 +1546,7 @@ class KotlinInputAstVisitor( | |
|
||
val delegationCall = constructor.getDelegationCall() | ||
visitFunctionLikeExpression( | ||
constructor.getStubOrPsiChild(KtStubElementTypes.CONTEXT_RECEIVER_LIST), | ||
constructor.modifierList, | ||
"constructor", | ||
null, | ||
|
@@ -1638,6 +1652,20 @@ class KotlinInputAstVisitor( | |
builder.forcedBreak() | ||
} | ||
|
||
/** Example `context(Logger, Raise<Error>)` */ | ||
override fun visitContextReceiverList(contextReceiverList: KtContextReceiverList) { | ||
builder.sync(contextReceiverList) | ||
builder.token("context") | ||
visitEachCommaSeparated( | ||
contextReceiverList.contextReceivers(), | ||
prefix = "(", | ||
postfix = ")", | ||
breakAfterPrefix = false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You probably don't want to specify these. They override the defaults to say "never break before or after the prefix, regardless of line length". |
||
breakBeforePostfix = false | ||
) | ||
builder.forcedBreak() | ||
} | ||
|
||
/** For example `@Magic private final` */ | ||
override fun visitModifierList(list: KtModifierList) { | ||
builder.sync(list) | ||
|
@@ -2427,6 +2455,7 @@ class KotlinInputAstVisitor( | |
override fun visitScript(script: KtScript) { | ||
markForPartialFormat() | ||
var lastChildHadBlankLineBefore = false | ||
var lastChildIsContextReceiver = false | ||
var first = true | ||
for (child in script.blockExpression.children) { | ||
if (child.text.isBlank()) { | ||
|
@@ -2436,13 +2465,16 @@ class KotlinInputAstVisitor( | |
val childGetsBlankLineBefore = child !is KtProperty | ||
if (first) { | ||
builder.blankLineWanted(OpsBuilder.BlankLineWanted.PRESERVE) | ||
} else if (lastChildIsContextReceiver) { | ||
builder.blankLineWanted(OpsBuilder.BlankLineWanted.NO) | ||
} else if (child !is PsiComment && | ||
(childGetsBlankLineBefore || lastChildHadBlankLineBefore)) { | ||
builder.blankLineWanted(OpsBuilder.BlankLineWanted.YES) | ||
} | ||
visit(child) | ||
builder.guessToken(";") | ||
lastChildHadBlankLineBefore = childGetsBlankLineBefore | ||
lastChildIsContextReceiver = child is KtScriptInitializer && child.firstChild?.firstChild?.firstChild?.text == "context" | ||
first = false | ||
} | ||
markForPartialFormat() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6674,6 +6674,48 @@ class FormatterTest { | |
assertThatFormatting(code).isEqualTo(expected) | ||
} | ||
|
||
@Test | ||
fun `context receivers`() { | ||
val code = | ||
""" | ||
|context(Something) | ||
| | ||
|class A { | ||
| context( | ||
| // Test comment. | ||
| Logger, Raise<Error>) | ||
| | ||
| @SomeAnnotation | ||
| | ||
| fun doNothing() {} | ||
| | ||
| context(SomethingElse) | ||
| | ||
| private class NestedClass {} | ||
|} | ||
|""" | ||
.trimMargin() | ||
|
||
val expected = | ||
""" | ||
|context(Something) | ||
|class A { | ||
| context( | ||
| // Test comment. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There should be some indentation here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 to this and your other comments in this test. Don't think any of it is blocking though. |
||
| Logger, | ||
| Raise<Error>) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is very subtle, but I'm specifically curious what happens when there's a comment between the last receiver type and the paren. That positioning is often buggy. |
||
| @SomeAnnotation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the context receivers break to multiple lines, I'd expect the list of annotations to also break. The repo owners may disagree though. Either way, there should be a test for the case of multiple annotations. |
||
| fun doNothing() {} | ||
| | ||
| context(SomethingElse) | ||
| private class NestedClass {} | ||
|} | ||
|""" | ||
.trimMargin() | ||
|
||
assertThatFormatting(code).isEqualTo(expected) | ||
} | ||
|
||
companion object { | ||
/** Triple quotes, useful to use within triple-quoted strings. */ | ||
private const val TQ = "\"\"\"" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -110,4 +110,28 @@ class TokenizerTest { | |
.containsExactly(0, -1, 1, 2, 3, -1, 4, -1, 5, 6, 7) | ||
.inOrder() | ||
} | ||
|
||
@Test | ||
fun `Context receivers are parsed correctly`() { | ||
val code = """ | ||
|context(Something) | ||
|class A { | ||
| context( | ||
| // Test comment. | ||
| Logger, Raise<Error>) | ||
| fun test() {} | ||
|} | ||
|""".trimMargin().trimMargin() | ||
|
||
val file = Parser.parse(code) | ||
val tokenizer = Tokenizer(code, file) | ||
file.accept(tokenizer) | ||
|
||
assertThat(tokenizer.toks.map { it.originalText }) | ||
.containsExactly("context", "(", "Something", ")", "\n", "class", " ", "A", " ", "{", "\n", " ", "context", "(", "\n", " ", "// Test comment.", "\n", " ", "Logger", ",", " ", "Raise", "<", "Error", ">", ")", "\n", " ", "fun", " ", "test", "(", ")", " ", "{", "}", "\n", "}") | ||
.inOrder() | ||
assertThat(tokenizer.toks.map { it.index }) | ||
.containsExactly(0, 1, 2, 3, -1, 4, -1, 5, -1, 6, -1, -1, 7, 8, -1, -1, 9, -1, -1, 10, 11, -1, 12, 13, 14, 15, 16, -1, -1, 17, -1, 18, 19, 20, -1, 21, 22, -1, 23) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: not a problem for this PR, but this unit test file isn't very maintainable. We should add some helpers to make it so that it's clear what's actually being tested, and how the numbers relate to the strings. |
||
.inOrder() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may have missed it, but there don't seem to be changes for context receivers on lambda parameters: