PY-20987 Consider multiple "glued" string nodes in the spell checker
[idea/community.git] / python / src / com / jetbrains / python / spellchecker / PythonSpellcheckerStrategy.java
1 /*
2  * Copyright 2000-2014 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.spellchecker;
17
18 import com.intellij.lang.ASTNode;
19 import com.intellij.openapi.util.Pair;
20 import com.intellij.openapi.util.TextRange;
21 import com.intellij.openapi.util.text.StringUtil;
22 import com.intellij.psi.PsiElement;
23 import com.intellij.spellchecker.inspections.PlainTextSplitter;
24 import com.intellij.spellchecker.inspections.Splitter;
25 import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy;
26 import com.intellij.spellchecker.tokenizer.TokenConsumer;
27 import com.intellij.spellchecker.tokenizer.Tokenizer;
28 import com.intellij.util.containers.ContainerUtil;
29 import com.jetbrains.python.PyTokenTypes;
30 import com.jetbrains.python.PythonStringUtil;
31 import com.jetbrains.python.inspections.PyStringFormatParser;
32 import com.jetbrains.python.psi.PyBinaryExpression;
33 import com.jetbrains.python.psi.PyStringLiteralExpression;
34 import org.jetbrains.annotations.NotNull;
35
36 import java.util.List;
37
38 import static com.jetbrains.python.psi.PyUtil.StringNodeInfo;
39
40 /**
41  * @author yole
42  */
43 public class PythonSpellcheckerStrategy extends SpellcheckingStrategy {
44   private static class StringLiteralTokenizer extends Tokenizer<PyStringLiteralExpression> {
45     @Override
46     public void tokenize(@NotNull PyStringLiteralExpression element, TokenConsumer consumer) {
47       final Splitter splitter = PlainTextSplitter.getInstance();
48       final List<ASTNode> strNodes = element.getStringNodes();
49       final List<String> prefixes = ContainerUtil.mapNotNull(strNodes, n -> StringUtil.nullize(new StringNodeInfo(n).getPrefix()));
50       
51       if (element.textContains('\\') && !prefixes.stream().anyMatch(PythonStringUtil::isRawPrefix)) {
52         for (Pair<TextRange, String> fragment : element.getDecodedFragments()) {
53           final String value = fragment.getSecond();
54           final int startOffset = fragment.getFirst().getStartOffset();
55           consumer.consumeToken(element, value, false, startOffset, TextRange.allOf(value), splitter);
56         }
57       }
58       else if (!prefixes.isEmpty()) {
59         for (TextRange valueTextRange : element.getStringValueTextRanges()) {
60           final String value = valueTextRange.substring(element.getText());
61           final int startOffset = valueTextRange.getStartOffset();
62           consumer.consumeToken(element, value, false, startOffset, TextRange.allOf(value), splitter);
63         }
64       }
65       else {
66         consumer.consumeToken(element, splitter);
67       }
68     }
69   }
70
71   private static class FormatStringTokenizer extends Tokenizer<PyStringLiteralExpression> {
72     @Override
73     public void tokenize(@NotNull PyStringLiteralExpression element, TokenConsumer consumer) {
74       String stringValue = element.getStringValue();
75       List<PyStringFormatParser.FormatStringChunk> chunks = PyStringFormatParser.parsePercentFormat(stringValue);
76       Splitter splitter = PlainTextSplitter.getInstance();
77       for (PyStringFormatParser.FormatStringChunk chunk : chunks) {
78         if (chunk instanceof PyStringFormatParser.ConstantChunk) {
79           int startIndex = element.valueOffsetToTextOffset(chunk.getStartIndex());
80           int endIndex = element.valueOffsetToTextOffset(chunk.getEndIndex());
81           String text = element.getText().substring(startIndex, endIndex);
82           consumer.consumeToken(element, text, false, startIndex, TextRange.allOf(text), splitter);
83         }
84       }
85     }
86   }
87
88   private StringLiteralTokenizer myStringLiteralTokenizer = new StringLiteralTokenizer();
89   private FormatStringTokenizer myFormatStringTokenizer = new FormatStringTokenizer();
90
91   @NotNull
92   @Override
93   public Tokenizer getTokenizer(PsiElement element) {
94     if (element instanceof PyStringLiteralExpression) {
95       PsiElement parent = element.getParent();
96       if (parent instanceof PyBinaryExpression) {
97         PyBinaryExpression binaryExpression = (PyBinaryExpression)parent;
98         if (element == binaryExpression.getLeftExpression() && binaryExpression.getOperator() == PyTokenTypes.PERC) {
99           return myFormatStringTokenizer;
100         }
101       }
102       return myStringLiteralTokenizer;
103     }
104     return super.getTokenizer(element);
105   }
106 }