diff --git a/python/psi-api/src/com/jetbrains/python/psi/PyElementVisitor.java b/python/psi-api/src/com/jetbrains/python/psi/PyElementVisitor.java index 91e43c7044e1..8bbfa949a0c3 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/PyElementVisitor.java +++ b/python/psi-api/src/com/jetbrains/python/psi/PyElementVisitor.java @@ -213,6 +213,14 @@ public class PyElementVisitor extends PsiElementVisitor { visitPyElement(node); } + public void visitPyFormattedStringNode(PyFormattedStringNode node) { + visitPyElement(node); + } + + public void visitPyFStringFragment(PyFStringFragment node) { + visitPyElement(node); + } + public void visitPyNumericLiteralExpression(final PyNumericLiteralExpression node) { visitPyElement(node); } diff --git a/python/psi-api/src/com/jetbrains/python/psi/PyFStringFragment.java b/python/psi-api/src/com/jetbrains/python/psi/PyFStringFragment.java index f6e81e860ca2..ba8766b5271e 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/PyFStringFragment.java +++ b/python/psi-api/src/com/jetbrains/python/psi/PyFStringFragment.java @@ -1,22 +1,24 @@ // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.psi; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; - -public interface PyFStringFragment extends PsiElement { +public interface PyFStringFragment extends PyElement { @Nullable - PyExpression getMainExpression(); + PyExpression getExpression(); @NotNull - List getFormatFragments(); + TextRange getExpressionContentRange(); + + @Nullable + PsiElement getTypeConversion(); @Nullable - PsiElement getColon(); - + PyFStringFragmentFormatPart getFormatPart(); + @Nullable PsiElement getClosingBrace(); } diff --git a/python/psi-api/src/com/jetbrains/python/psi/PyFStringFragmentFormatPart.java b/python/psi-api/src/com/jetbrains/python/psi/PyFStringFragmentFormatPart.java new file mode 100644 index 000000000000..0717c7a6fdb5 --- /dev/null +++ b/python/psi-api/src/com/jetbrains/python/psi/PyFStringFragmentFormatPart.java @@ -0,0 +1,11 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python.psi; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public interface PyFStringFragmentFormatPart extends PyElement { + @NotNull + List getFragments(); +} diff --git a/python/psi-api/src/com/jetbrains/python/psi/PyFormattedStringNode.java b/python/psi-api/src/com/jetbrains/python/psi/PyFormattedStringNode.java index 5269a5721e28..d42a4255db7e 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/PyFormattedStringNode.java +++ b/python/psi-api/src/com/jetbrains/python/psi/PyFormattedStringNode.java @@ -1,6 +1,7 @@ // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.psi; +import com.intellij.openapi.util.TextRange; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -8,4 +9,8 @@ import java.util.List; public interface PyFormattedStringNode extends PyRichStringNode { @NotNull List getFragments(); + + @NotNull + List getLiteralPartRanges(); + } diff --git a/python/psi-api/src/com/jetbrains/python/psi/PyRichStringNode.java b/python/psi-api/src/com/jetbrains/python/psi/PyRichStringNode.java index 470ea1e9b093..233c8e288d61 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/PyRichStringNode.java +++ b/python/psi-api/src/com/jetbrains/python/psi/PyRichStringNode.java @@ -2,7 +2,6 @@ package com.jetbrains.python.psi; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -11,7 +10,7 @@ import java.util.Set; /** * @author Mikhail Golubev */ -public interface PyRichStringNode extends PsiElement { +public interface PyRichStringNode extends PyElement { enum Modifier { UNICODE, diff --git a/python/src/com/jetbrains/python/psi/PyStringLiteralUtil.java b/python/src/com/jetbrains/python/psi/PyStringLiteralUtil.java index 18d1a333b0de..5bed103aadb8 100644 --- a/python/src/com/jetbrains/python/psi/PyStringLiteralUtil.java +++ b/python/src/com/jetbrains/python/psi/PyStringLiteralUtil.java @@ -74,7 +74,8 @@ public class PyStringLiteralUtil { public static boolean isStringLiteralToken(@NotNull String text) { final PythonLexer lexer = new PythonLexer(); lexer.start(text); - return PyTokenTypes.STRING_NODES.contains(lexer.getTokenType()) && lexer.getTokenEnd() == lexer.getBufferEnd(); + return PyTokenTypes.STRING_NODES.contains(lexer.getTokenType()) && lexer.getTokenEnd() == lexer.getBufferEnd() || + PyTokenTypes.FSTRING_START == lexer.getTokenType(); } /** diff --git a/python/src/com/jetbrains/python/psi/PyUtil.java b/python/src/com/jetbrains/python/psi/PyUtil.java index 5d853bd92484..8e6e48c07aae 100644 --- a/python/src/com/jetbrains/python/psi/PyUtil.java +++ b/python/src/com/jetbrains/python/psi/PyUtil.java @@ -43,12 +43,14 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.WindowManager; import com.intellij.psi.*; import com.intellij.psi.stubs.StubElement; +import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.*; import com.intellij.ui.awt.RelativePoint; import com.intellij.util.*; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.NotNullPredicate; import com.jetbrains.python.PyBundle; +import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyTokenTypes; import com.jetbrains.python.codeInsight.completion.OverwriteEqualsInsertHandler; @@ -1914,8 +1916,10 @@ public class PyUtil { private final TextRange myContentRange; public StringNodeInfo(@NotNull ASTNode node) { - if (!PyTokenTypes.STRING_NODES.contains(node.getElementType())) { - throw new IllegalArgumentException("Node must be valid Python string literal token, but " + node.getElementType() + " was given"); + final IElementType nodeType = node.getElementType(); + // TODO Migrate to newer PyRichStringNode API + if (!PyTokenTypes.STRING_NODES.contains(nodeType) && nodeType != PyElementTypes.FSTRING_NODE) { + throw new IllegalArgumentException("Node must be valid Python string literal token, but " + nodeType + " was given"); } myNode = node; final String nodeText = node.getText(); diff --git a/python/src/com/jetbrains/python/psi/impl/PyFStringFragmentFormatPartImpl.java b/python/src/com/jetbrains/python/psi/impl/PyFStringFragmentFormatPartImpl.java index cb96fa70aa18..e7219da20c70 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyFStringFragmentFormatPartImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyFStringFragmentFormatPartImpl.java @@ -2,9 +2,21 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; +import com.jetbrains.python.PyElementTypes; +import com.jetbrains.python.psi.PyFStringFragment; +import com.jetbrains.python.psi.PyFStringFragmentFormatPart; +import org.jetbrains.annotations.NotNull; -public class PyFStringFragmentFormatPartImpl extends PyElementImpl { +import java.util.List; + +public class PyFStringFragmentFormatPartImpl extends PyElementImpl implements PyFStringFragmentFormatPart { public PyFStringFragmentFormatPartImpl(ASTNode astNode) { super(astNode); } + + @NotNull + @Override + public List getFragments() { + return findChildrenByType(PyElementTypes.FSTRING_FRAGMENT); + } } diff --git a/python/src/com/jetbrains/python/psi/impl/PyFStringFragmentImpl.java b/python/src/com/jetbrains/python/psi/impl/PyFStringFragmentImpl.java index 180fbf2a44b7..8232a9c3ac4c 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyFStringFragmentImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyFStringFragmentImpl.java @@ -2,37 +2,50 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; -import com.jetbrains.python.PyElementTypes; +import com.intellij.util.ObjectUtils; import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.psi.PyElementVisitor; import com.jetbrains.python.psi.PyExpression; import com.jetbrains.python.psi.PyFStringFragment; +import com.jetbrains.python.psi.PyFStringFragmentFormatPart; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; - public class PyFStringFragmentImpl extends PyElementImpl implements PyFStringFragment { public PyFStringFragmentImpl(ASTNode astNode) { super(astNode); } + @Override + protected void acceptPyVisitor(PyElementVisitor pyVisitor) { + pyVisitor.visitPyFStringFragment(this); + } + @Nullable @Override - public PyExpression getMainExpression() { + public PyExpression getExpression() { return findChildByClass(PyExpression.class); } @NotNull @Override - public List getFormatFragments() { - return findChildrenByType(PyElementTypes.FSTRING_FRAGMENT); + public TextRange getExpressionContentRange() { + final PsiElement endAnchor = ObjectUtils.coalesce(getTypeConversion(), getFormatPart(), getClosingBrace()); + return TextRange.create(1, endAnchor != null ? endAnchor.getStartOffsetInParent(): getTextLength()); } @Nullable @Override - public PsiElement getColon() { - return findChildByType(PyTokenTypes.COLON); + public PsiElement getTypeConversion() { + return findChildByType(PyTokenTypes.FSTRING_FRAGMENT_TYPE_CONVERSION); + } + + @Nullable + @Override + public PyFStringFragmentFormatPart getFormatPart() { + return findChildByClass(PyFStringFragmentFormatPart.class); } @Nullable diff --git a/python/src/com/jetbrains/python/psi/impl/PyFormattedStringNodeImpl.java b/python/src/com/jetbrains/python/psi/impl/PyFormattedStringNodeImpl.java index b06d49f014d5..bc2cba11934d 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyFormattedStringNodeImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyFormattedStringNodeImpl.java @@ -7,9 +7,11 @@ import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiErrorElement; +import com.intellij.psi.SyntaxTraverser; import com.intellij.psi.tree.IElementType; import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.psi.PyElementVisitor; import com.jetbrains.python.psi.PyFStringFragment; import com.jetbrains.python.psi.PyFormattedStringNode; import com.jetbrains.python.psi.PyStringLiteralUtil; @@ -26,12 +28,30 @@ public class PyFormattedStringNodeImpl extends PyElementImpl implements PyFormat super(astNode); } + @Override + protected void acceptPyVisitor(PyElementVisitor pyVisitor) { + pyVisitor.visitPyFormattedStringNode(this); + } + @NotNull @Override public List getFragments() { return findChildrenByType(PyElementTypes.FSTRING_FRAGMENT); } + @NotNull + @Override + public List getLiteralPartRanges() { + final int nodeStart = getTextRange().getStartOffset(); + final TextRange contentRange = getContentRange(); + return SyntaxTraverser.psiApi() + .children(this) + .filter(child -> child.getNode().getElementType() == PyTokenTypes.FSTRING_TEXT) + .map(part -> part.getTextRange().shiftLeft(nodeStart)) + .map(range -> range.intersection(contentRange)) + .toList(); + } + @NotNull @Override public String getPrefix() { diff --git a/python/src/com/jetbrains/python/validation/FStringsAnnotator.java b/python/src/com/jetbrains/python/validation/FStringsAnnotator.java index e15ce2f7086a..2747b4dc9a06 100644 --- a/python/src/com/jetbrains/python/validation/FStringsAnnotator.java +++ b/python/src/com/jetbrains/python/validation/FStringsAnnotator.java @@ -15,87 +15,93 @@ */ package com.jetbrains.python.validation; -import com.intellij.lang.ASTNode; +import com.google.common.collect.Lists; import com.intellij.openapi.util.TextRange; -import com.intellij.util.text.CharArrayUtil; -import com.jetbrains.python.codeInsight.fstrings.FStringParser; -import com.jetbrains.python.codeInsight.fstrings.FStringParser.Fragment; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.python.psi.PyFStringFragment; +import com.jetbrains.python.psi.PyFStringFragmentFormatPart; +import com.jetbrains.python.psi.PyFormattedStringNode; import com.jetbrains.python.psi.PyStringLiteralExpression; -import org.jetbrains.annotations.NotNull; -import static com.jetbrains.python.psi.PyUtil.StringNodeInfo; +import java.util.List; /** * @author Mikhail Golubev */ public class FStringsAnnotator extends PyAnnotator { - @Override - public void visitPyStringLiteralExpression(PyStringLiteralExpression pyString) { - for (ASTNode node : pyString.getStringNodes()) { - final StringNodeInfo nodeInfo = new StringNodeInfo(node); - final String nodeText = node.getText(); - if (nodeInfo.isFormatted()) { - final int nodeContentEnd = nodeInfo.getContentRange().getEndOffset(); - final FStringParser.ParseResult result = FStringParser.parse(nodeText); - TextRange unclosedBraceRange = null; - for (Fragment fragment : result.getFragments()) { - final int fragLeftBrace = fragment.getLeftBraceOffset(); - final int fragContentEnd = fragment.getContentEndOffset(); - final int fragRightBrace = fragment.getRightBraceOffset(); - final TextRange wholeFragmentRange = TextRange.create(fragLeftBrace, fragRightBrace == -1 ? nodeContentEnd : fragRightBrace + 1); - if (fragment.getDepth() > 2) { - // Do not report anything about expression fragments nested deeper that three times - if (fragment.getDepth() == 3) { - report("Expression fragment inside f-string is nested too deeply", wholeFragmentRange, node); - } - continue; + @Override + public void visitPyFStringFragment(PyFStringFragment node) { + final List enclosingFragments = PsiTreeUtil.collectParents(node, PyFStringFragment.class, false, + PyStringLiteralExpression.class::isInstance); + if (enclosingFragments.size() > 1) { + report(node, "Expression fragment inside f-string is nested too deeply"); + } + final PsiElement typeConversion = node.getTypeConversion(); + if (typeConversion != null) { + final String conversionChar = typeConversion.getText().substring(1); + if (conversionChar.isEmpty()) { + report(typeConversion, "Conversion character is expected: should be one of 's', 'r', 'a'"); + } + else if (conversionChar.length() > 1 || "sra".indexOf(conversionChar.charAt(0)) < 0) { + report(typeConversion, "Illegal conversion character '" + conversionChar + "': should be one of 's', 'r', 'a'"); + } + } + + final boolean topLevel = PsiTreeUtil.getParentOfType(node, PyFStringFragment.class, true) == null; + if (topLevel) { + final List fragments = Lists.newArrayList(node); + final PyFStringFragmentFormatPart formatPart = node.getFormatPart(); + if (formatPart != null) { + fragments.addAll(formatPart.getFragments()); + } + for (PyFStringFragment fragment : fragments) { + final String wholeNodeText = fragment.getText(); + final TextRange range = fragment.getExpressionContentRange(); + for (int i = range.getStartOffset(); i < range.getEndOffset(); i++) { + if (wholeNodeText.charAt(i) == '\\') { + reportCharacter(fragment, i, "Expression fragments inside f-strings cannot include backslashes"); } - if (CharArrayUtil.isEmptyOrSpaces(nodeText, fragLeftBrace + 1, fragContentEnd) && fragContentEnd < nodeContentEnd) { - final TextRange range = TextRange.create(fragLeftBrace, fragContentEnd + 1); - report("Empty expression fragments are not allowed inside f-strings", range, node); - } - if (fragRightBrace == -1 && unclosedBraceRange == null) { - unclosedBraceRange = wholeFragmentRange; - } - if (fragment.getFirstHashOffset() != -1) { - final TextRange range = TextRange.create(fragment.getFirstHashOffset(), fragment.getContentEndOffset()); - report("Expression fragments inside f-strings cannot include line comments", range, node); - } - for (int i = fragLeftBrace + 1; i < fragment.getContentEndOffset(); i++) { - if (nodeText.charAt(i) == '\\') { - reportCharacter("Expression fragments inside f-strings cannot include backslashes", i, node); - } - } - // Do not warn about illegal conversion character if '!' is right before closing quotes - if (fragContentEnd < nodeContentEnd && nodeText.charAt(fragContentEnd) == '!' && fragContentEnd + 1 < nodeContentEnd) { - final char conversionChar = nodeText.charAt(fragContentEnd + 1); - // No conversion character -- highlight only "!" - if (fragContentEnd + 1 == fragRightBrace || conversionChar == ':') { - reportCharacter("Conversion character is expected: should be one of 's', 'r', 'a'", fragContentEnd, node); - } - // Wrong conversion character -- highlight both "!" and the following symbol - else if ("sra".indexOf(conversionChar) < 0) { - final TextRange range = TextRange.from(fragContentEnd, 2); - report("Illegal conversion character '" + conversionChar + "': should be one of 's', 'r', 'a'", range, node); - } - } - } - for (Integer offset : result.getSingleRightBraces()) { - reportCharacter("Single '}' is not allowed inside f-strings", offset, node); - } - if (unclosedBraceRange != null) { - report("'}' is expected", unclosedBraceRange, node); } } } } - private void report(@NotNull String message, @NotNull TextRange range, @NotNull ASTNode node) { - getHolder().createErrorAnnotation(range.shiftRight(node.getTextRange().getStartOffset()), message); + @Override + public void visitPyFormattedStringNode(PyFormattedStringNode node) { + final String wholeNodeText = node.getText(); + for (TextRange textRange : node.getLiteralPartRanges()) { + int i = textRange.getStartOffset(); + while (i < textRange.getEndOffset()) { + final char c = wholeNodeText.charAt(i); + if (c == '}') { + if (i + 1 < textRange.getEndOffset() && wholeNodeText.charAt(i + 1) == '}') { + i += 2; + continue; + } + reportCharacter(node, i, "Single '}' is not allowed inside f-strings"); + } + i++; + } + } } - private void reportCharacter(@NotNull String message, int offset, @NotNull ASTNode node) { - report(message, TextRange.from(offset, 1), node); + @Override + public void visitComment(PsiComment comment) { + final boolean insideFragment = PsiTreeUtil.getParentOfType(comment, PyFStringFragment.class) != null; + if (insideFragment) { + report(comment, "Expression fragments inside f-strings cannot include line comments"); + } + } + + public void reportCharacter(PsiElement element, int offset, String message) { + final int nodeStartOffset = element.getTextRange().getStartOffset(); + getHolder().createErrorAnnotation(TextRange.from(offset, 1).shiftRight(nodeStartOffset), message); + } + + public void report(PsiElement element, String error) { + getHolder().createErrorAnnotation(element, error); } } diff --git a/python/src/com/jetbrains/python/validation/StringLiteralQuotesAnnotator.java b/python/src/com/jetbrains/python/validation/StringLiteralQuotesAnnotator.java index b59380c4f7c5..fb18ef128d2f 100644 --- a/python/src/com/jetbrains/python/validation/StringLiteralQuotesAnnotator.java +++ b/python/src/com/jetbrains/python/validation/StringLiteralQuotesAnnotator.java @@ -5,6 +5,7 @@ import com.intellij.lang.ASTNode; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.jetbrains.python.PyBundle; +import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.psi.PyStringLiteralExpression; import com.jetbrains.python.psi.PyStringLiteralUtil; import com.jetbrains.python.psi.impl.PyPsiUtils; @@ -25,6 +26,10 @@ public class StringLiteralQuotesAnnotator extends PyAnnotator { public void visitPyStringLiteralExpression(final PyStringLiteralExpression node) { final List stringNodes = node.getStringNodes(); for (ASTNode stringNode : stringNodes) { + // TODO Migrate to newer PyRichStringNode API + if (stringNode.getElementType() == PyElementTypes.FSTRING_NODE) { + continue; + } final String nodeText = PyPsiUtils.getElementTextWithoutHostEscaping(stringNode.getPsi()); final int index = PyStringLiteralUtil.getPrefixLength(nodeText); final String unprefixed = nodeText.substring(index); diff --git a/python/testData/highlighting/fStringBackslashes.py b/python/testData/highlighting/fStringBackslashes.py index 9bd478c0d46a..18a278fc55cd 100644 --- a/python/testData/highlighting/fStringBackslashes.py +++ b/python/testData/highlighting/fStringBackslashes.py @@ -1,5 +1,5 @@ -f'{\t}' -f'{\t' -f'{\N{GREEK SMALL LETTER ALPHA}}' +f'{\t}' +f'{\t' +f'{\N{GREEK SMALL LETTER ALPHA}}' f'{Formatable():\n\t}' -f'{42:{\t}}' \ No newline at end of file +f'{42:{\t}}' \ No newline at end of file diff --git a/python/testData/highlighting/fStringEmptyExpressions.py b/python/testData/highlighting/fStringEmptyExpressions.py index 0600fc8bcecb..174b985398a9 100644 --- a/python/testData/highlighting/fStringEmptyExpressions.py +++ b/python/testData/highlighting/fStringEmptyExpressions.py @@ -1,10 +1,10 @@ -f'{}' -f'{' -f'{ -f'{!r}' -f'{:2.3}' -f'{42:2.{}}' -f'{ }' -f'{42:{ }}' -f'{ :{ ' -f'{ !r:{ :42}}' \ No newline at end of file +f'{}' +f'{' +f'{ +f'{!r}' +f'{:2.3}' +f'{42:2.{}}' +f'{ }' +f'{42:{ }}' +f'{ :{ ' +f'{ !r:{ :42}}' \ No newline at end of file diff --git a/python/testData/highlighting/fStringHashSigns.py b/python/testData/highlighting/fStringHashSigns.py index e9d185c838fc..892e4319fa05 100644 --- a/python/testData/highlighting/fStringHashSigns.py +++ b/python/testData/highlighting/fStringHashSigns.py @@ -1,7 +1,10 @@ -f'{#' -f'{# -f'{#foo#}' -f'{42:#}' -f'{42:{#}}' -f'{x ### foo}' -f'{"###"}' \ No newline at end of file +f'{#' +f'{# +f'{#foo#}' +f'{42:#}' +f'{42:{#}}' +f'{x ### foo}' +f'{"###"}' +f'''{[ + 42 # foo +]}''' diff --git a/python/testData/highlighting/fStringIllegalConversionCharacter.py b/python/testData/highlighting/fStringIllegalConversionCharacter.py index 1a8f5df1192c..085e74162367 100644 --- a/python/testData/highlighting/fStringIllegalConversionCharacter.py +++ b/python/testData/highlighting/fStringIllegalConversionCharacter.py @@ -2,8 +2,8 @@ f'{42!r}' f'{42!s}' f'{42!a}' f'{42!z}' -f'{42!foo}' +f'{42!foo}' f'{42!}' f'{42!:2}' -f'{42!' -f'{42! +f'{42!' +f'{42! diff --git a/python/testData/highlighting/fStringMissingRightBrace.py b/python/testData/highlighting/fStringMissingRightBrace.py index cf292f995b5e..fc774b0aaebd 100644 --- a/python/testData/highlighting/fStringMissingRightBrace.py +++ b/python/testData/highlighting/fStringMissingRightBrace.py @@ -3,8 +3,8 @@ f'{42!r}' f'{42!r:03}' f'{42:03}' f'{42!r:{y}.{z}}' -f'{' -f'{42:{' -f'{42!r:{' +f'{' +f'{42:{' +f'{42!r:{' f'{{' -f'{{{' \ No newline at end of file +f'{{{' \ No newline at end of file diff --git a/python/testData/highlighting/fStringTooDeeplyNestedExpressionFragments.py b/python/testData/highlighting/fStringTooDeeplyNestedExpressionFragments.py index 93b7bae81feb..6b3714527833 100644 --- a/python/testData/highlighting/fStringTooDeeplyNestedExpressionFragments.py +++ b/python/testData/highlighting/fStringTooDeeplyNestedExpressionFragments.py @@ -1,7 +1,7 @@ -f'{x:{y:{}}}' -f'{x:{y:{# foo}}}' -f'{x:{y:{z!z}}}' -f'{x:{y:{z:{42}}}}' -f'{:{:{:{}}}}' -f'{x:{y:{z' -f'{x:{y:{z \ No newline at end of file +f'{x:{y:{}}}' +f'{x:{y:{# foo}}}' +f'{x:{y:{z!z}}}' +f'{x:{y:{z:{42}}}}' +f'{:{:{:{}}}}' +f'{x:{y:{z' +f'{x:{y:{z \ No newline at end of file diff --git a/python/testData/psi/FStringDeeplyNestedEmptyFragments.py b/python/testData/psi/FStringDeeplyNestedEmptyFragments.py new file mode 100644 index 000000000000..4a5abb4b43a1 --- /dev/null +++ b/python/testData/psi/FStringDeeplyNestedEmptyFragments.py @@ -0,0 +1 @@ +s = f'{:{:{:{}}}}' \ No newline at end of file diff --git a/python/testData/psi/FStringDeeplyNestedEmptyFragments.txt b/python/testData/psi/FStringDeeplyNestedEmptyFragments.txt new file mode 100644 index 000000000000..aebab178d631 --- /dev/null +++ b/python/testData/psi/FStringDeeplyNestedEmptyFragments.txt @@ -0,0 +1,37 @@ +PyFile:FStringDeeplyNestedEmptyFragments.py + PyAssignmentStatement + PyTargetExpression: s + PsiElement(Py:IDENTIFIER)('s') + PsiWhiteSpace(' ') + PsiElement(Py:EQ)('=') + PsiWhiteSpace(' ') + PyStringLiteralExpression: {:{:{:{}}}} + PyFormattedStringNode + PsiElement(Py:FSTRING_START)('f'') + PyFStringFragment + PsiElement(Py:FSTRING_FRAGMENT_START)('{') + PsiErrorElement:expression expected + + PyFStringFragmentFormatPart + PsiElement(Py:FSTRING_FRAGMENT_FORMAT_START)(':') + PyFStringFragment + PsiElement(Py:FSTRING_FRAGMENT_START)('{') + PsiErrorElement:expression expected + + PyFStringFragmentFormatPart + PsiElement(Py:FSTRING_FRAGMENT_FORMAT_START)(':') + PyFStringFragment + PsiElement(Py:FSTRING_FRAGMENT_START)('{') + PsiErrorElement:expression expected + + PyFStringFragmentFormatPart + PsiElement(Py:FSTRING_FRAGMENT_FORMAT_START)(':') + PyFStringFragment + PsiElement(Py:FSTRING_FRAGMENT_START)('{') + PsiErrorElement:expression expected + + PsiElement(Py:FSTRING_FRAGMENT_END)('}') + PsiElement(Py:FSTRING_FRAGMENT_END)('}') + PsiElement(Py:FSTRING_FRAGMENT_END)('}') + PsiElement(Py:FSTRING_FRAGMENT_END)('}') + PsiElement(Py:FSTRING_END)(''') \ No newline at end of file diff --git a/python/testData/psi/MultilineFStringContainingMultilineExpressionAfterStatementBreak.py b/python/testData/psi/MultilineFStringContainingMultilineExpressionAfterStatementBreak.py new file mode 100644 index 000000000000..730263e7d443 --- /dev/null +++ b/python/testData/psi/MultilineFStringContainingMultilineExpressionAfterStatementBreak.py @@ -0,0 +1,3 @@ +pass +f'''{1 + +2}''' diff --git a/python/testData/psi/MultilineFStringContainingMultilineExpressionAfterStatementBreak.txt b/python/testData/psi/MultilineFStringContainingMultilineExpressionAfterStatementBreak.txt new file mode 100644 index 000000000000..86a567b46c54 --- /dev/null +++ b/python/testData/psi/MultilineFStringContainingMultilineExpressionAfterStatementBreak.txt @@ -0,0 +1,21 @@ +PyFile:MultilineFStringContainingMultilineExpressionAfterStatementBreak.py + PyPassStatement + PsiElement(Py:PASS_KEYWORD)('pass') + PsiWhiteSpace('\n') + PyExpressionStatement + PyStringLiteralExpression: {1 + +2} + PyFormattedStringNode + PsiElement(Py:FSTRING_START)('f'''') + PyFStringFragment + PsiElement(Py:FSTRING_FRAGMENT_START)('{') + PyBinaryExpression + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('1') + PsiWhiteSpace(' ') + PsiElement(Py:PLUS)('+') + PsiWhiteSpace(' \n') + PyNumericLiteralExpression + PsiElement(Py:INTEGER_LITERAL)('2') + PsiElement(Py:FSTRING_FRAGMENT_END)('}') + PsiElement(Py:FSTRING_END)(''''') \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PythonParsingTest.java b/python/testSrc/com/jetbrains/python/PythonParsingTest.java index b5607db4fed2..b94650ecccb0 100644 --- a/python/testSrc/com/jetbrains/python/PythonParsingTest.java +++ b/python/testSrc/com/jetbrains/python/PythonParsingTest.java @@ -821,6 +821,10 @@ public class PythonParsingTest extends ParsingTestCase { doTest(LanguageLevel.PYTHON36); } + public void testFStringDeeplyNestedEmptyFragments() { + doTest(LanguageLevel.PYTHON36); + } + // PY-19036 public void testAwaitInNonAsyncNestedFunction() { doTest(LanguageLevel.PYTHON35);