Permalink
Browse files

Move to struct assignment intention (#2806)

fixes #2702
  • Loading branch information...
1 parent 0632a7c commit 0bfabe5f4af95cfca9ddc395542e898c732cce23 @grenki grenki committed with ignatov Oct 27, 2016
Showing with 1,033 additions and 3 deletions.
  1. +5 −0 resources/META-INF/gogland.xml
  2. +1 −0 resources/intentionDescriptions/GoMoveToStructInitializationIntention/after.go.template
  3. +2 −0 resources/intentionDescriptions/GoMoveToStructInitializationIntention/before.go.template
  4. +21 −0 resources/intentionDescriptions/GoMoveToStructInitializationIntention/description.html
  5. +1 −1 src/com/goide/inspections/GoStructInitializationInspection.java
  6. +292 −0 src/com/goide/intentions/GoMoveToStructInitializationIntention.java
  7. +2 −2 src/com/goide/psi/impl/GoElementFactory.java
  8. +20 −0 src/com/goide/psi/impl/GoPsiImplUtil.java
  9. +11 −0 testData/intentions/move-to-struct-initialization/anonymousField-after.go
  10. +11 −0 testData/intentions/move-to-struct-initialization/anonymousField.go
  11. +13 −0 testData/intentions/move-to-struct-initialization/caretAtValue-after.go
  12. +13 −0 testData/intentions/move-to-struct-initialization/caretAtValue.go
  13. +12 −0 testData/intentions/move-to-struct-initialization/duplicateFields-after.go
  14. +12 −0 testData/intentions/move-to-struct-initialization/duplicateFields.go
  15. +11 −0 testData/intentions/move-to-struct-initialization/existingDeclaration.go
  16. +12 −0 testData/intentions/move-to-struct-initialization/existingField-after.go
  17. +12 −0 testData/intentions/move-to-struct-initialization/existingField.go
  18. +12 −0 testData/intentions/move-to-struct-initialization/fieldExchange-after.go
  19. +12 −0 testData/intentions/move-to-struct-initialization/fieldExchange.go
  20. +17 −0 testData/intentions/move-to-struct-initialization/invalidAssignment-after.go
  21. +17 −0 testData/intentions/move-to-struct-initialization/invalidAssignment.go
  22. +15 −0 testData/intentions/move-to-struct-initialization/justAssignedVar-after.go
  23. +15 −0 testData/intentions/move-to-struct-initialization/justAssignedVar.go
  24. +15 −0 testData/intentions/move-to-struct-initialization/justAssignedVarWrongCaret.go
  25. +12 −0 testData/intentions/move-to-struct-initialization/justInitializedVar-after.go
  26. +12 −0 testData/intentions/move-to-struct-initialization/justInitializedVar.go
  27. +13 −0 testData/intentions/move-to-struct-initialization/justInitializedVarWrongCaret.go
  28. +16 −0 testData/intentions/move-to-struct-initialization/multiReturnFunction.go
  29. +14 −0 testData/intentions/move-to-struct-initialization/multipleAssignmentsLeftmost-after.go
  30. +14 −0 testData/intentions/move-to-struct-initialization/multipleAssignmentsLeftmost.go
  31. +14 −0 testData/intentions/move-to-struct-initialization/multipleAssignmentsMiddle-after.go
  32. +14 −0 testData/intentions/move-to-struct-initialization/multipleAssignmentsMiddle.go
  33. +14 −0 testData/intentions/move-to-struct-initialization/multipleAssignmentsRightmost-after.go
  34. +14 −0 testData/intentions/move-to-struct-initialization/multipleAssignmentsRightmost.go
  35. +16 −0 testData/intentions/move-to-struct-initialization/multipleFields-after.go
  36. +16 −0 testData/intentions/move-to-struct-initialization/multipleFields.go
  37. +12 −0 testData/intentions/move-to-struct-initialization/multipleFieldsPartlyAssigned-after.go
  38. +12 −0 testData/intentions/move-to-struct-initialization/multipleFieldsPartlyAssigned.go
  39. +14 −0 testData/intentions/move-to-struct-initialization/multipleFieldsSameStructureCaretAtValue-after.go
  40. +14 −0 testData/intentions/move-to-struct-initialization/multipleFieldsSameStructureCaretAtValue.go
  41. +11 −0 testData/intentions/move-to-struct-initialization/notExistingField.go
  42. +11 −0 testData/intentions/move-to-struct-initialization/oneLineFieldDeclaration-after.go
  43. +11 −0 testData/intentions/move-to-struct-initialization/oneLineFieldDeclaration.go
  44. +11 −0 testData/intentions/move-to-struct-initialization/simple-after.go
  45. +11 −0 testData/intentions/move-to-struct-initialization/simple.go
  46. +12 −0 testData/intentions/move-to-struct-initialization/structAssignment-after.go
  47. +12 −0 testData/intentions/move-to-struct-initialization/structAssignment.go
  48. +15 −0 testData/intentions/move-to-struct-initialization/structAssignmentMultipleAssignee-after.go
  49. +15 −0 testData/intentions/move-to-struct-initialization/structAssignmentMultipleAssignee.go
  50. +12 −0 testData/intentions/move-to-struct-initialization/twoSameStructures-after.go
  51. +12 −0 testData/intentions/move-to-struct-initialization/twoSameStructures.go
  52. +13 −0 testData/intentions/move-to-struct-initialization/twoSameStructuresAssignment-after.go
  53. +13 −0 testData/intentions/move-to-struct-initialization/twoSameStructuresAssignment.go
  54. +11 −0 testData/intentions/move-to-struct-initialization/withParens-after.go
  55. +11 −0 testData/intentions/move-to-struct-initialization/withParens.go
  56. +16 −0 testData/intentions/move-to-struct-initialization/wrongStruct.go
  57. +61 −0 tests/com/goide/quickfix/GoMoveToStructInitializationIntentionTest.java
@@ -181,6 +181,11 @@
<categoryKey>go.intentions.category</categoryKey>
<className>com.goide.intentions.GoAddFunctionBlockIntention</className>
</intentionAction>
+ <intentionAction>
+ <bundleName>com.goide.GoBundle</bundleName>
+ <categoryKey>go.intentions.category</categoryKey>
+ <className>com.goide.intentions.GoMoveToStructInitializationIntention</className>
+ </intentionAction>
<!-- unused inspections -->
<localInspection language="go" displayName="Unused import inspection" groupPath="Go"
groupName="Declaration redundancy" enabledByDefault="true" level="ERROR"
@@ -0,0 +1 @@
+s := S{<spot>foo: `bar`</spot>}
@@ -0,0 +1,2 @@
+s := S{}
+<spot>s.foo = `bar`</spot>
@@ -0,0 +1,21 @@
+<!--
+ ~ Copyright 2013-2016 Sergey Ignatov, Alexander Zolotov, Florin Patan
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<html>
+<body>
+This intention moves struct field assignment to struct initialization block.
+</body>
+</html>
@@ -108,7 +108,7 @@ public GoReplaceWithNamedStructFieldQuickFix(@NotNull String structField) {
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiElement startElement = descriptor.getStartElement();
if (startElement instanceof GoElement) {
- startElement.replace(GoElementFactory.createNamedStructField(project, myStructField, startElement.getText()));
+ startElement.replace(GoElementFactory.createLiteralValueElement(project, myStructField, startElement.getText()));
}
}
}
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2013-2016 Sergey Ignatov, Alexander Zolotov, Florin Patan
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.goide.intentions;
+
+import com.goide.psi.*;
+import com.goide.psi.impl.GoElementFactory;
+import com.goide.psi.impl.GoPsiImplUtil;
+import com.intellij.codeInsight.intention.BaseElementAtCaretIntentionAction;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.IncorrectOperationException;
+import com.intellij.util.ObjectUtils;
+import com.intellij.util.containers.MultiMap;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Set;
+
+import static com.intellij.util.containers.ContainerUtil.*;
+
+public class GoMoveToStructInitializationIntention extends BaseElementAtCaretIntentionAction {
+ public static final String NAME = "Move field assignment to struct initialization";
+
+ public GoMoveToStructInitializationIntention() {
+ setText(NAME);
+ }
+
+ @Nls
+ @NotNull
+ @Override
+ public String getFamilyName() {
+ return NAME;
+ }
+
+ @Override
+ public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) {
+ return getData(element) != null;
+ }
+
+ @Nullable
+ private static Data getData(@NotNull PsiElement element) {
+ if (!element.isValid() || !element.isWritable()) return null;
+ GoAssignmentStatement assignment = getValidAssignmentParent(element);
+ GoReferenceExpression selectedFieldReference = assignment != null ? getFieldReferenceExpression(element, assignment) : null;
+ GoCompositeLit compositeLit = selectedFieldReference != null ? getStructLiteralByReference(selectedFieldReference, assignment) : null;
+ if (compositeLit == null) return null;
+
+ List<GoReferenceExpression> references = getUninitializedSingleFieldReferences(assignment, selectedFieldReference, compositeLit);
+ return !references.isEmpty() ? new Data(assignment, compositeLit, references) : null;
+ }
+
+ @Nullable
+ private static GoAssignmentStatement getValidAssignmentParent(@Nullable PsiElement element) {
+ GoAssignmentStatement assignment = PsiTreeUtil.getNonStrictParentOfType(element, GoAssignmentStatement.class);
+ return assignment != null && assignment.isValid() && getLeftHandElements(assignment).size() == assignment.getExpressionList().size()
+ ? assignment : null;
+ }
+
+ @Nullable
+ private static GoReferenceExpression getFieldReferenceExpression(@NotNull PsiElement selectedElement,
+ @NotNull GoAssignmentStatement assignment) {
+ GoReferenceExpression selectedReferenceExpression = PsiTreeUtil.getTopmostParentOfType(selectedElement, GoReferenceExpression.class);
+ if (isFieldReferenceExpression(selectedReferenceExpression)) {
+ return !isAssignedInPreviousStatement(selectedReferenceExpression, assignment) ? selectedReferenceExpression : null;
+ }
+
+ List<GoReferenceExpression> fieldReferenceExpressions = getFieldReferenceExpressions(assignment);
+ if (exists(fieldReferenceExpressions, expression -> isAssignedInPreviousStatement(expression, assignment))) return null;
+
+ Set<PsiElement> resolvedFields = map2Set(fieldReferenceExpressions, GoMoveToStructInitializationIntention::resolveQualifier);
+ return resolvedFields.size() == 1 ? getFirstItem(fieldReferenceExpressions) : null;
+ }
+
+ @NotNull
+ private static List<GoReferenceExpression> getFieldReferenceExpressions(@NotNull GoAssignmentStatement assignment) {
+ return filter(map(getLeftHandElements(assignment), GoMoveToStructInitializationIntention::unwrapParensAndCast),
+ GoMoveToStructInitializationIntention::isFieldReferenceExpression);
+ }
+
+ @Nullable
+ private static GoReferenceExpression unwrapParensAndCast(@NotNull PsiElement e) {
+ while (e instanceof GoParenthesesExpr) {
+ e = ((GoParenthesesExpr)e).getExpression();
+ }
+ return ObjectUtils.tryCast(e, GoReferenceExpression.class);
+ }
+
+ @Contract("null -> false")
+ private static boolean isFieldReferenceExpression(@Nullable PsiElement element) {
+ return element instanceof GoReferenceExpression && isFieldDefinition(((GoReferenceExpression)element).resolve());
+ }
+
+ @Contract("null -> false")
+ private static boolean isFieldDefinition(@Nullable PsiElement element) {
+ return element instanceof GoFieldDefinition || element instanceof GoAnonymousFieldDefinition;
+ }
+
+ private static boolean isAssignedInPreviousStatement(@NotNull GoExpression referenceExpression,
+ @NotNull GoAssignmentStatement assignment) {
+ GoReferenceExpression rightExpression = ObjectUtils.tryCast(GoPsiImplUtil.getRightExpression(assignment, referenceExpression),
+ GoReferenceExpression.class);
+
+ PsiElement resolve = rightExpression != null ? rightExpression.resolve() : null;
+ GoStatement previousElement = resolve != null ? PsiTreeUtil.getPrevSiblingOfType(assignment, GoStatement.class) : null;
+ return previousElement != null && exists(getLeftHandElements(previousElement), e -> isResolvedTo(e, resolve));
+ }
+
+ private static boolean isResolvedTo(@Nullable PsiElement e, @Nullable PsiElement resolve) {
+ if (e instanceof GoVarDefinition) return resolve == e;
+
+ GoReferenceExpression refExpression = ObjectUtils.tryCast(e, GoReferenceExpression.class);
+ return refExpression != null && refExpression.resolve() == resolve;
+ }
+
+ @NotNull
+ private static List<GoReferenceExpression> getUninitializedSingleFieldReferences(@NotNull GoAssignmentStatement assignment,
+ @NotNull GoReferenceExpression fieldReferenceExpression,
+ @NotNull GoCompositeLit compositeLit) {
+ PsiElement resolve = resolveQualifier(fieldReferenceExpression);
+ List<GoReferenceExpression> uninitializedFieldReferencesByQualifier =
+ filter(getUninitializedFieldReferenceExpressions(assignment, compositeLit), e -> isResolvedTo(e.getQualifier(), resolve));
+ MultiMap<PsiElement, GoReferenceExpression> resolved = groupBy(uninitializedFieldReferencesByQualifier, GoReferenceExpression::resolve);
+ return map(filter(resolved.entrySet(), set -> set.getValue().size() == 1), set -> getFirstItem(set.getValue()));
+ }
+
+ @Nullable
+ private static GoCompositeLit getStructLiteralByReference(@NotNull GoReferenceExpression fieldReferenceExpression,
+ @NotNull GoAssignmentStatement assignment) {
+ GoStatement previousStatement = PsiTreeUtil.getPrevSiblingOfType(assignment, GoStatement.class);
+ if (previousStatement instanceof GoSimpleStatement) {
+ return getStructLiteral(fieldReferenceExpression, (GoSimpleStatement)previousStatement);
+ }
+ if (previousStatement instanceof GoAssignmentStatement) {
+ return getStructLiteral(fieldReferenceExpression, (GoAssignmentStatement)previousStatement);
+ }
+ return null;
+ }
+
+ @Nullable
+ private static GoCompositeLit getStructLiteral(@NotNull GoReferenceExpression fieldReferenceExpression,
+ @NotNull GoSimpleStatement structDeclaration) {
+ GoShortVarDeclaration varDeclaration = structDeclaration.getShortVarDeclaration();
+ if (varDeclaration == null) return null;
+
+ PsiElement resolve = resolveQualifier(fieldReferenceExpression);
+ GoVarDefinition structVarDefinition = find(varDeclaration.getVarDefinitionList(), definition -> resolve == definition);
+ return structVarDefinition != null ? ObjectUtils.tryCast(structVarDefinition.getValue(), GoCompositeLit.class) : null;
+ }
+
+ @Nullable
+ private static PsiElement resolveQualifier(@NotNull GoReferenceExpression fieldReferenceExpression) {
+ GoReferenceExpression qualifier = fieldReferenceExpression.getQualifier();
+ return qualifier != null ? qualifier.resolve() : null;
+ }
+
+ @Nullable
+ private static GoCompositeLit getStructLiteral(@NotNull GoReferenceExpression fieldReferenceExpression,
+ @NotNull GoAssignmentStatement structAssignment) {
+ GoVarDefinition varDefinition = ObjectUtils.tryCast(resolveQualifier(fieldReferenceExpression), GoVarDefinition.class);
+ PsiElement field = fieldReferenceExpression.resolve();
+ if (varDefinition == null || !isFieldDefinition(field) || !hasStructTypeWithField(varDefinition, (GoNamedElement)field)) {
+ return null;
+ }
+
+ GoExpression structReferenceExpression = find(structAssignment.getLeftHandExprList().getExpressionList(),
+ expression -> isResolvedTo(expression, varDefinition));
+ if (structReferenceExpression == null) return null;
+ GoExpression compositeLit = GoPsiImplUtil.getRightExpression(structAssignment, structReferenceExpression);
+ return ObjectUtils.tryCast(compositeLit, GoCompositeLit.class);
+ }
+
+ private static boolean hasStructTypeWithField(@NotNull GoVarDefinition structVarDefinition, @NotNull GoNamedElement field) {
+ GoType type = structVarDefinition.getGoType(null);
+ GoStructType structType = type != null ? ObjectUtils.tryCast(type.getUnderlyingType(), GoStructType.class) : null;
+ return structType != null && PsiTreeUtil.isAncestor(structType, field, true);
+ }
+
+ private static boolean isFieldInitialization(@NotNull GoElement element, @NotNull PsiElement field) {
+ GoKey key = element.getKey();
+ GoFieldName fieldName = key != null ? key.getFieldName() : null;
+ return fieldName != null && fieldName.resolve() == field;
+ }
+
+ @NotNull
+ private static List<GoReferenceExpression> getUninitializedFieldReferenceExpressions(@NotNull GoAssignmentStatement assignment,
+ @NotNull GoCompositeLit structLiteral) {
+ return filter(getFieldReferenceExpressions(assignment), expression ->
+ isUninitializedFieldReferenceExpression(expression, structLiteral) && !isAssignedInPreviousStatement(expression, assignment));
+ }
+
+ @Contract("null, _-> false")
+ private static boolean isUninitializedFieldReferenceExpression(@Nullable GoReferenceExpression fieldReferenceExpression,
+ @NotNull GoCompositeLit structLiteral) {
+ if (fieldReferenceExpression == null) return false;
+ GoLiteralValue literalValue = structLiteral.getLiteralValue();
+ PsiElement resolve = fieldReferenceExpression.resolve();
+ return literalValue != null && isFieldDefinition(resolve) &&
+ !exists(literalValue.getElementList(), element -> isFieldInitialization(element, resolve));
+ }
+
+ @NotNull
+ private static List<? extends PsiElement> getLeftHandElements(@NotNull GoStatement statement) {
+ if (statement instanceof GoSimpleStatement) {
+ GoShortVarDeclaration varDeclaration = ((GoSimpleStatement)statement).getShortVarDeclaration();
+ return varDeclaration != null ? varDeclaration.getVarDefinitionList() : emptyList();
+ }
+ if (statement instanceof GoAssignmentStatement) {
+ return ((GoAssignmentStatement)statement).getLeftHandExprList().getExpressionList();
+ }
+ return emptyList();
+ }
+
+ @Override
+ public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException {
+ Data data = getData(element);
+ if (data == null) return;
+ moveFieldReferenceExpressions(data);
+ }
+
+ private static void moveFieldReferenceExpressions(@NotNull Data data) {
+ GoLiteralValue literalValue = data.getCompositeLit().getLiteralValue();
+ if (literalValue == null) return;
+
+ for (GoReferenceExpression expression : data.getReferenceExpressions()) {
+ GoExpression parentExpression = PsiTreeUtil.getTopmostParentOfType(expression, GoExpression.class);
+ GoExpression anchor = parentExpression != null ? parentExpression : expression;
+ GoExpression fieldValue = GoPsiImplUtil.getRightExpression(data.getAssignment(), anchor);
+ if (fieldValue == null) continue;
+
+ GoPsiImplUtil.deleteExpressionFromAssignment(data.getAssignment(), anchor);
+ addFieldDefinition(literalValue, expression.getIdentifier().getText(), fieldValue.getText());
+ }
+ }
+
+ private static void addFieldDefinition(@NotNull GoLiteralValue literalValue, @NotNull String name, @NotNull String value) {
+ Project project = literalValue.getProject();
+ PsiElement newField = GoElementFactory.createLiteralValueElement(project, name, value);
+ GoElement lastElement = getLastItem(literalValue.getElementList());
+ if (lastElement == null) {
+ literalValue.addAfter(newField, literalValue.getLbrace());
+ }
+ else {
+ lastElement.add(GoElementFactory.createComma(project));
+ lastElement.add(newField);
+ }
+ }
+
+ private static class Data {
+ private final GoCompositeLit myCompositeLit;
+ private final GoAssignmentStatement myAssignment;
+ private final List<GoReferenceExpression> myReferenceExpressions;
+
+ public Data(@NotNull GoAssignmentStatement assignment,
+ @NotNull GoCompositeLit compositeLit,
+ @NotNull List<GoReferenceExpression> referenceExpressions) {
+ myCompositeLit = compositeLit;
+ myAssignment = assignment;
+ myReferenceExpressions = referenceExpressions;
+ }
+
+ public GoCompositeLit getCompositeLit() {
+ return myCompositeLit;
+ }
+
+ public GoAssignmentStatement getAssignment() {
+ return myAssignment;
+ }
+
+ public List<GoReferenceExpression> getReferenceExpressions() {
+ return myReferenceExpressions;
+ }
+ }
+}
@@ -256,8 +256,8 @@ public static GoType createType(@NotNull Project project, @NotNull String text)
return PsiTreeUtil.findChildOfType(file, GoType.class);
}
- public static PsiElement createNamedStructField(@NotNull Project project, @NotNull String field, @NotNull String element) {
- GoFile file = createFileFromText(project, "package a; var _ = struct { a string } { " + field + ": " + element + " }");
+ public static PsiElement createLiteralValueElement(@NotNull Project project, @NotNull String key, @NotNull String value) {
+ GoFile file = createFileFromText(project, "package a; var _ = struct { a string } { " + key + ": " + value + " }");
return PsiTreeUtil.findChildOfType(file, GoElement.class);
}
@@ -1354,6 +1354,20 @@ public static void deleteSpec(@NotNull GoConstDeclaration declaration, @NotNull
specToDelete.delete();
}
+ public static void deleteExpressionFromAssignment(@NotNull GoAssignmentStatement assignment,
+ @NotNull GoExpression expressionToDelete) {
+ GoExpression expressionValue = getRightExpression(assignment, expressionToDelete);
+ if (expressionValue != null) {
+ if (assignment.getExpressionList().size() == 1) {
+ assignment.delete();
+ }
+ else {
+ deleteElementFromCommaSeparatedList(expressionToDelete);
+ deleteElementFromCommaSeparatedList(expressionValue);
+ }
+ }
+ }
+
public static void deleteDefinition(@NotNull GoVarSpec spec, @NotNull GoVarDefinition definitionToDelete) {
List<GoVarDefinition> definitionList = spec.getVarDefinitionList();
int index = definitionList.indexOf(definitionToDelete);
@@ -1679,4 +1693,10 @@ private static PsiElement getNotNullElement(@Nullable PsiElement... elements) {
public static boolean isSingleCharLiteral(@NotNull GoStringLiteral literal) {
return literal.getDecodedText().length() == 1;
}
+
+ @Nullable
+ public static GoExpression getRightExpression(@NotNull GoAssignmentStatement assignment, @NotNull GoExpression leftExpression) {
+ int fieldIndex = assignment.getLeftHandExprList().getExpressionList().indexOf(leftExpression);
+ return getByIndex(assignment.getExpressionList(), fieldIndex);
+ }
}
@@ -0,0 +1,11 @@
+package main
+
+type S struct {
+ string
+}
+
+func main() {
+ s := S{string: "bar"}
+ _ = "foo"
+ print(s.string)
+}
Oops, something went wrong.

0 comments on commit 0bfabe5

Please sign in to comment.