2 * Copyright 2000-2016 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.jetbrains.python.validation;
18 import com.intellij.lang.ASTNode;
19 import com.intellij.openapi.util.TextRange;
20 import com.intellij.util.text.CharArrayUtil;
21 import com.jetbrains.python.codeInsight.fstrings.FStringParser;
22 import com.jetbrains.python.codeInsight.fstrings.FStringParser.FragmentOffsets;
23 import com.jetbrains.python.psi.PyStringLiteralExpression;
24 import org.jetbrains.annotations.NotNull;
26 import java.util.List;
28 import static com.jetbrains.python.psi.PyUtil.StringNodeInfo;
31 * @author Mikhail Golubev
33 public class FStringsAnnotator extends PyAnnotator {
35 public void visitPyStringLiteralExpression(PyStringLiteralExpression pyString) {
36 for (ASTNode node : pyString.getStringNodes()) {
37 final StringNodeInfo nodeInfo = new StringNodeInfo(node);
38 final String nodeText = node.getText();
39 if (nodeInfo.isFormatted()) {
40 final int nodeOffset = node.getTextRange().getStartOffset();
41 final int nodeContentEnd = nodeInfo.getContentRange().getEndOffset();
42 final List<FragmentOffsets> fragments = FStringParser.parse(nodeText);
43 boolean hasUnclosedBrace = false;
44 for (FragmentOffsets fragment : fragments) {
45 final int fragContentEnd = fragment.getContentEndOffset();
46 if (CharArrayUtil.isEmptyOrSpaces(nodeText, fragment.getLeftBraceOffset() + 1, fragment.getContentEndOffset())) {
47 report(fragment.getContentRange().shiftRight(nodeOffset), "Empty expressions are not allowed inside f-strings");
49 if (fragment.getRightBraceOffset() == -1) {
50 hasUnclosedBrace = true;
52 // Do not warn about illegal conversion character if '!' is right before closing quotes
53 if (fragContentEnd < nodeContentEnd && nodeText.charAt(fragContentEnd) == '!' && fragContentEnd + 1 < nodeContentEnd) {
54 final char conversionChar = nodeText.charAt(fragContentEnd + 1);
55 final int offset = fragContentEnd + nodeOffset + 1;
56 if (fragContentEnd + 1 == fragment.getRightBraceOffset() || conversionChar == ':') {
57 reportEmpty(offset, "Conversion character is expected: should be one of 's', 'r', 'a'");
59 else if ("sra".indexOf(conversionChar) < 0) {
60 reportCharacter(offset, "Illegal conversion character '" + conversionChar + "': should be one of 's', 'r', 'a'");
64 if (hasUnclosedBrace) {
65 reportEmpty(nodeContentEnd + nodeOffset, "'}' is expected");
71 private void report(@NotNull TextRange range, @NotNull String message) {
72 getHolder().createErrorAnnotation(range, message);
75 private void reportEmpty(int offset, @NotNull String message) {
76 getHolder().createErrorAnnotation(TextRange.from(offset, 0), message);
79 private void reportCharacter(int offset, @NotNull String message) {
80 getHolder().createErrorAnnotation(TextRange.from(offset, 1), message);