diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index 52f5e888d55e..d49c98fe9604 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -644,6 +644,7 @@ + diff --git a/python/src/com/jetbrains/python/validation/FStringsAnnotator.java b/python/src/com/jetbrains/python/validation/FStringsAnnotator.java new file mode 100644 index 000000000000..5cfed9a5565d --- /dev/null +++ b/python/src/com/jetbrains/python/validation/FStringsAnnotator.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * 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.jetbrains.python.validation; + +import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; +import com.jetbrains.python.codeInsight.fstrings.FStringParser; +import com.jetbrains.python.codeInsight.fstrings.FStringParser.FragmentOffsets; +import com.jetbrains.python.psi.PyStringLiteralExpression; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import static com.jetbrains.python.psi.PyUtil.StringNodeInfo; + +/** + * @author Mikhail Golubev + */ +public class FStringsAnnotator extends PyAnnotator { + @Override + public void visitPyStringLiteralExpression(PyStringLiteralExpression pyString) { + for (ASTNode node : pyString.getStringNodes()) { + if (new StringNodeInfo(node).isFormatted()) { + final int nodeOffset = node.getTextRange().getStartOffset(); + final List fragments = FStringParser.parse(node.getText()); + for (FragmentOffsets fragment : fragments) { + if (fragment.getLeftBraceOffset() + 1 >= fragment.getContentEndOffset()) { + report(fragment.getContentRange().shiftRight(nodeOffset), "Empty expressions are not allowed inside f-strings"); + } + } + } + } + } + + private void report(@NotNull TextRange range, @NotNull String message) { + getHolder().createErrorAnnotation(range, message); + } +} diff --git a/python/testData/highlighting/fStringEmptyExpressions.py b/python/testData/highlighting/fStringEmptyExpressions.py new file mode 100644 index 000000000000..e3fa03973e5a --- /dev/null +++ b/python/testData/highlighting/fStringEmptyExpressions.py @@ -0,0 +1,6 @@ +f'{}' +f'{' +f'{ +f'{!r}' +f'{:2.3}' +f'{42:2.{}}' \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java b/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java index 25f1fcff2fc3..5678d380ecfc 100644 --- a/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java +++ b/python/testSrc/com/jetbrains/python/PythonHighlightingTest.java @@ -305,6 +305,11 @@ public class PythonHighlightingTest extends PyTestCase { doTest(); } + // PY-20776 + public void testFStringEmptyExpressions() { + runWithLanguageLevel(LanguageLevel.PYTHON36, () -> doTest(true, false)); + } + // --- private void doTest(final LanguageLevel languageLevel, final boolean checkWarnings, final boolean checkInfos) { PythonLanguageLevelPusher.setForcedLanguageLevel(myFixture.getProject(), languageLevel);