PY-20776 Warn expression parts that contain only whitespaces
[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           // 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'");
58             }
59             else if ("sra".indexOf(conversionChar) < 0) {
60               reportCharacter(offset, "Illegal conversion character '" + conversionChar + "': should be one of 's', 'r', 'a'");
61             }
62           }
63         }
64         if (hasUnclosedBrace) {
65           reportEmpty(nodeContentEnd + nodeOffset, "'}' is expected");
66         }
67       }
68     }
69   }
70
71   private void report(@NotNull TextRange range, @NotNull String message) {
72     getHolder().createErrorAnnotation(range, message);
73   }
74
75   private void reportEmpty(int offset, @NotNull String message) {
76     getHolder().createErrorAnnotation(TextRange.from(offset, 0), message);
77   }
78
79   private void reportCharacter(int offset, @NotNull String message) {
80     getHolder().createErrorAnnotation(TextRange.from(offset, 1), message);
81   }
82 }