PY-31442 Migrate FStringsAnnotator to the new f-strings AST

Also tweaked interfaces of PyFormattedStringNode and PyFStringFragment.
This commit is contained in:
Mikhail Golubev
2018-09-05 18:58:36 +03:00
parent 6749fd59a2
commit 2f9b1c62ff
23 changed files with 276 additions and 121 deletions

View File

@@ -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);
}

View File

@@ -1,21 +1,23 @@
// 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<PyFStringFragment> getFormatFragments();
TextRange getExpressionContentRange();
@Nullable
PsiElement getColon();
PsiElement getTypeConversion();
@Nullable
PyFStringFragmentFormatPart getFormatPart();
@Nullable
PsiElement getClosingBrace();

View File

@@ -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<PyFStringFragment> getFragments();
}

View File

@@ -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<PyFStringFragment> getFragments();
@NotNull
List<TextRange> getLiteralPartRanges();
}

View File

@@ -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,

View File

@@ -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();
}
/**

View File

@@ -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();

View File

@@ -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<PyFStringFragment> getFragments() {
return findChildrenByType(PyElementTypes.FSTRING_FRAGMENT);
}
}

View File

@@ -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<PyFStringFragment> 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

View File

@@ -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<PyFStringFragment> getFragments() {
return findChildrenByType(PyElementTypes.FSTRING_FRAGMENT);
}
@NotNull
@Override
public List<TextRange> 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() {

View File

@@ -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);
@Override
public void visitPyFStringFragment(PyFStringFragment node) {
final List<PyFStringFragment> 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<PyFStringFragment> 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");
}
}
}
}
}
@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;
}
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);
reportCharacter(node, i, "Single '}' is not allowed inside f-strings");
}
i++;
}
}
}
private void report(@NotNull String message, @NotNull TextRange range, @NotNull ASTNode node) {
getHolder().createErrorAnnotation(range.shiftRight(node.getTextRange().getStartOffset()), message);
@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");
}
}
private void reportCharacter(@NotNull String message, int offset, @NotNull ASTNode node) {
report(message, TextRange.from(offset, 1), node);
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);
}
}

View File

@@ -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<ASTNode> 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);

View File

@@ -1,5 +1,5 @@
f'{<error descr="Expression fragments inside f-strings cannot include backslashes">\</error>t}'
f'<error descr="'}' is expected">{<error descr="Expression fragments inside f-strings cannot include backslashes">\</error>t</error>'
f'{<error descr="Expression fragments inside f-strings cannot include backslashes">\</error>N{GREEK SMALL LETTER ALPHA}}'
f'{<error descr="expression expected"><error descr="Expression fragments inside f-strings cannot include backslashes">\</error>t</error>}'
f'{<error descr="expression expected"><error descr="Expression fragments inside f-strings cannot include backslashes">\</error>t</error><error descr="type conversion, : or } expected">'</error>
f'{<error descr="expression expected"><error descr="Expression fragments inside f-strings cannot include backslashes">\</error>N{GREEK SMALL LETTER ALPHA}</error>}'
f'{Formatable():\n\t}'
f'{42:{<error descr="Expression fragments inside f-strings cannot include backslashes">\</error>t}}'
f'{42:{<error descr="expression expected"><error descr="Expression fragments inside f-strings cannot include backslashes">\</error>t</error>}}'

View File

@@ -1,10 +1,10 @@
f'<error descr="Empty expression fragments are not allowed inside f-strings">{}</error>'
f'<error descr="'}' is expected">{</error>'
<error descr="Missing closing quote [']">f'<error descr="'}' is expected">{</error></error>
f'<error descr="Empty expression fragments are not allowed inside f-strings">{!</error>r}'
f'<error descr="Empty expression fragments are not allowed inside f-strings">{:</error>2.3}'
f'{42:2.<error descr="Empty expression fragments are not allowed inside f-strings">{}</error>}'
f'<error descr="Empty expression fragments are not allowed inside f-strings">{ }</error>'
f'{42:<error descr="Empty expression fragments are not allowed inside f-strings">{ }</error>}'
f'<error descr="'}' is expected"><error descr="Empty expression fragments are not allowed inside f-strings">{ :</error>{ </error>'
f'<error descr="Empty expression fragments are not allowed inside f-strings">{ !</error>r:<error descr="Empty expression fragments are not allowed inside f-strings">{ :</error>42}}'
f'{<error descr="expression expected">}</error>'
f'{<error descr="expression expected"><error descr="type conversion, : or } expected">'</error></error>
f'{<EOLError descr="type conversion, : or } expected"></EOLError><EOLError descr="expression expected"></EOLError><EOLError descr="' expected"></EOLError>
f'{<error descr="expression expected">!</error>r}'
f'{<error descr="expression expected">:</error>2.3}'
f'{42:2.{<error descr="expression expected">}</error>}'
f'{<error descr="expression expected"> </error> }'
f'{42:{<error descr="expression expected"> </error>}}'
f'{<error descr="expression expected"> </error> :{<error descr="expression expected"><error descr="type conversion, : or } expected"> </error></error> '
f'{<error descr="expression expected"> </error> !r:{<error descr="expression expected"> </error> :42}}'

View File

@@ -1,7 +1,10 @@
f'<error descr="'}' is expected">{<error descr="Expression fragments inside f-strings cannot include line comments">#</error></error>'
<error descr="Missing closing quote [']">f'<error descr="'}' is expected">{<error descr="Expression fragments inside f-strings cannot include line comments">#</error></error></error>
f'{<error descr="Expression fragments inside f-strings cannot include line comments">#foo#</error>}'
f'{42:#}'
f'{42:{<error descr="Expression fragments inside f-strings cannot include line comments">#</error>}}'
f'{x <error descr="Expression fragments inside f-strings cannot include line comments">### foo</error>}'
f'{<error descr="' expected"><error descr="expression expected"><error descr="type conversion, : or } expected">#</error></error></error>'
f'{<error descr="' expected"><error descr="expression expected"><error descr="type conversion, : or } expected">#</error></error></error>
f'{<error descr="' expected"><error descr="expression expected"><error descr="type conversion, : or } expected">#</error></error></error>foo#}'
f'{42:<error descr="' expected"><error descr="} expected">#</error></error>}'
f'{42:{<error descr="' expected"><error descr="expression expected"><error descr="type conversion, : or } expected">#</error></error></error>}}'
f'{x<error descr="' expected"><error descr="type conversion, : or } expected"> </error></error>### foo}'
f'{"###"}'
f'''{[
42 <error descr="Expression fragments inside f-strings cannot include line comments"># foo</error>
]}'''

View File

@@ -2,8 +2,8 @@ f'{42!r}'
f'{42!s}'
f'{42!a}'
f'{42<error descr="Illegal conversion character 'z': should be one of 's', 'r', 'a'">!z</error>}'
f'{42<error descr="Illegal conversion character 'f': should be one of 's', 'r', 'a'">!f</error>oo}'
f'{42<error descr="Illegal conversion character 'foo': should be one of 's', 'r', 'a'">!foo</error>}'
f'{42<error descr="Conversion character is expected: should be one of 's', 'r', 'a'">!</error>}'
f'{42<error descr="Conversion character is expected: should be one of 's', 'r', 'a'">!</error>:2}'
f'<error descr="'}' is expected">{42!</error>'
<error descr="Missing closing quote [']">f'<error descr="'}' is expected">{42!</error></error>
f'{42<error descr="Conversion character is expected: should be one of 's', 'r', 'a'">!</error><error descr=": or } expected">'</error>
f'{42<error descr="Conversion character is expected: should be one of 's', 'r', 'a'">!</error><EOLError descr=": or } expected"></EOLError><EOLError descr="' expected"></EOLError>

View File

@@ -3,8 +3,8 @@ f'{42!r}'
f'{42!r:03}'
f'{42:03}'
f'{42!r:{y}.{z}}'
f'<error descr="'}' is expected">{</error>'
f'<error descr="'}' is expected">{42:{</error>'
f'<error descr="'}' is expected">{42!r:{</error>'
f'{<error descr="expression expected"><error descr="type conversion, : or } expected">'</error></error>
f'{42:{<error descr="expression expected"><error descr="type conversion, : or } expected">'</error></error>
f'{42!r:{<error descr="expression expected"><error descr="type conversion, : or } expected">'</error></error>
f'{{'
f'{{<error descr="'}' is expected">{</error>'
f'{{{<error descr="expression expected"><error descr="type conversion, : or } expected">'</error></error>

View File

@@ -1,7 +1,7 @@
f'{x:{y:<error descr="Expression fragment inside f-string is nested too deeply">{}</error>}}'
f'{x:{y:<error descr="Expression fragment inside f-string is nested too deeply">{# foo}</error>}}'
f'{x:{y:<error descr="Expression fragment inside f-string is nested too deeply">{z!z}</error>}}'
f'{x:{y:<error descr="Expression fragment inside f-string is nested too deeply">{z:{42}}</error>}}'
f'<error descr="Empty expression fragments are not allowed inside f-strings">{:</error><error descr="Empty expression fragments are not allowed inside f-strings">{:</error><error descr="Expression fragment inside f-string is nested too deeply">{:{}}</error>}}'
f'<error descr="'}' is expected">{x:{y:<error descr="Expression fragment inside f-string is nested too deeply">{z</error></error>'
<error descr="Missing closing quote [']">f'<error descr="'}' is expected">{x:{y:<error descr="Expression fragment inside f-string is nested too deeply">{z</error></error></error>
f'{x:{y:<error descr="Expression fragment inside f-string is nested too deeply">{<error descr="expression expected">}</error></error>}}'
f'{x:{y:<error descr="Expression fragment inside f-string is nested too deeply">{</error><error descr="' expected"><error descr="expression expected"><error descr="type conversion, : or } expected">#</error></error></error> foo}}}'
f'{x:{y:<error descr="Expression fragment inside f-string is nested too deeply">{z<error descr="Illegal conversion character 'z': should be one of 's', 'r', 'a'">!z</error>}</error>}}'
f'{x:{y:<error descr="Expression fragment inside f-string is nested too deeply">{z:<error descr="Expression fragment inside f-string is nested too deeply">{42}</error>}</error>}}'
f'{<error descr="expression expected">:</error>{<error descr="expression expected">:</error><error descr="Expression fragment inside f-string is nested too deeply">{<error descr="expression expected">:</error><error descr="Expression fragment inside f-string is nested too deeply">{<error descr="expression expected">}</error></error>}</error>}}'
f'{x:{y:<error descr="Expression fragment inside f-string is nested too deeply">{z</error><error descr="type conversion, : or } expected">'</error>
f'{x:{y:<error descr="Expression fragment inside f-string is nested too deeply">{z</error><EOLError descr="type conversion, : or } expected"></EOLError><EOLError descr="' expected"></EOLError>

View File

@@ -0,0 +1 @@
s = f'{:{:{:{}}}}'

View File

@@ -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
<empty list>
PyFStringFragmentFormatPart
PsiElement(Py:FSTRING_FRAGMENT_FORMAT_START)(':')
PyFStringFragment
PsiElement(Py:FSTRING_FRAGMENT_START)('{')
PsiErrorElement:expression expected
<empty list>
PyFStringFragmentFormatPart
PsiElement(Py:FSTRING_FRAGMENT_FORMAT_START)(':')
PyFStringFragment
PsiElement(Py:FSTRING_FRAGMENT_START)('{')
PsiErrorElement:expression expected
<empty list>
PyFStringFragmentFormatPart
PsiElement(Py:FSTRING_FRAGMENT_FORMAT_START)(':')
PyFStringFragment
PsiElement(Py:FSTRING_FRAGMENT_START)('{')
PsiErrorElement:expression expected
<empty list>
PsiElement(Py:FSTRING_FRAGMENT_END)('}')
PsiElement(Py:FSTRING_FRAGMENT_END)('}')
PsiElement(Py:FSTRING_FRAGMENT_END)('}')
PsiElement(Py:FSTRING_FRAGMENT_END)('}')
PsiElement(Py:FSTRING_END)(''')

View File

@@ -0,0 +1,3 @@
pass
f'''{1 +
2}'''

View File

@@ -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)(''''')

View File

@@ -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);