PY-20844 PY-20773 Warn about '#' signs and backslashes inside f-strings
[idea/community.git] / python / src / com / jetbrains / python / validation / FStringsAnnotator.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.jetbrains.python.validation;
17
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;
25
26 import java.util.List;
27
28 import static com.jetbrains.python.psi.PyUtil.StringNodeInfo;
29
30 /**
31  * @author Mikhail Golubev
32  */
33 public class FStringsAnnotator extends PyAnnotator {
34   @Override
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");
48           }
49           if (fragment.getRightBraceOffset() == -1) {
50             hasUnclosedBrace = true;
51           }
52           for (int i = fragment.getLeftBraceOffset() + 1; i < fragment.getContentEndOffset(); i++) {
53             final char c = nodeText.charAt(i);
54             if (c == '\\') {
55               reportCharacter(nodeOffset + i, "Expression fragments inside f-strings cannot include backslashes");
56             }
57             else if (c == '#') {
58               reportCharacter(nodeOffset + i, "Expressions fragments inside f-strings cannot include '#'");
59             }
60           }
61           // Do not warn about illegal conversion character if '!' is right before closing quotes 
62           if (fragContentEnd < nodeContentEnd && nodeText.charAt(fragContentEnd) == '!' && fragContentEnd + 1 < nodeContentEnd) {
63             final char conversionChar = nodeText.charAt(fragContentEnd + 1);
64             final int offset = fragContentEnd + nodeOffset + 1;
65             if (fragContentEnd + 1 == fragment.getRightBraceOffset() || conversionChar == ':') {
66               reportEmpty(offset, "Conversion character is expected: should be one of 's', 'r', 'a'");
67             }
68             else if ("sra".indexOf(conversionChar) < 0) {
69               reportCharacter(offset, "Illegal conversion character '" + conversionChar + "': should be one of 's', 'r', 'a'");
70             }
71           }
72         }
73         if (hasUnclosedBrace) {
74           reportEmpty(nodeContentEnd + nodeOffset, "'}' is expected");
75         }
76       }
77     }
78   }
79
80   private void report(@NotNull TextRange range, @NotNull String message) {
81     getHolder().createErrorAnnotation(range, message);
82   }
83
84   private void reportEmpty(int offset, @NotNull String message) {
85     getHolder().createErrorAnnotation(TextRange.from(offset, 0), message);
86   }
87
88   private void reportCharacter(int offset, @NotNull String message) {
89     getHolder().createErrorAnnotation(TextRange.from(offset, 1), message);
90   }
91 }