Cleanup: NotNull/Nullable
[idea/community.git] / java / java-psi-api / src / com / intellij / psi / PsiNameHelper.java
1 /*
2  * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
3  */
4 package com.intellij.psi;
5
6 import com.intellij.openapi.components.ServiceManager;
7 import com.intellij.openapi.project.Project;
8 import com.intellij.openapi.util.text.StringUtil;
9 import com.intellij.pom.java.LanguageLevel;
10 import com.intellij.psi.util.PsiTreeUtil;
11 import com.intellij.psi.util.PsiUtil;
12 import com.intellij.util.ArrayUtil;
13 import org.jetbrains.annotations.NotNull;
14 import org.jetbrains.annotations.Nullable;
15
16 import java.util.Arrays;
17 import java.util.List;
18 import java.util.regex.Pattern;
19
20 import static com.intellij.util.ObjectUtils.notNull;
21
22 /**
23  * Service for validating and parsing Java identifiers.
24  */
25 public abstract class PsiNameHelper {
26
27   public static PsiNameHelper getInstance(Project project) {
28     return ServiceManager.getService(project, PsiNameHelper.class);
29   }
30
31   /**
32    * Checks if the specified text is a Java identifier, using the language level of the project
33    * with which the name helper is associated to filter out keywords.
34    *
35    * @param text the text to check.
36    * @return true if the text is an identifier, false otherwise
37    */
38   public abstract boolean isIdentifier(@Nullable String text);
39
40   /**
41    * Checks if the specified text is a Java identifier, using the specified language level
42    * with which the name helper is associated to filter out keywords.
43    *
44    * @param text the text to check.
45    * @param languageLevel to check text against. For instance 'assert' or 'enum' might or might not be identifiers depending on language level
46    * @return true if the text is an identifier, false otherwise
47    */
48   public abstract boolean isIdentifier(@Nullable String text, @NotNull LanguageLevel languageLevel);
49
50   /**
51    * Checks if the specified text is a Java keyword, using the language level of the project
52    * with which the name helper is associated.
53    *
54    * @param text the text to check.
55    * @return true if the text is a keyword, false otherwise
56    */
57   public abstract boolean isKeyword(@Nullable String text);
58
59   /**
60    * Checks if the specified string is a qualified name (sequence of identifiers separated by
61    * periods).
62    *
63    * @param text the text to check.
64    * @return true if the text is a qualified name, false otherwise.
65    */
66   public abstract boolean isQualifiedName(@Nullable String text);
67
68   @NotNull
69   public static String getShortClassName(@NotNull String referenceText) {
70     int lessPos = referenceText.length();
71     int bracesBalance = 0;
72     int i;
73
74     loop:
75     for (i = referenceText.length() - 1; i >= 0; i--) {
76       char ch = referenceText.charAt(i);
77       switch (ch) {
78         case ')':
79         case '>':
80           bracesBalance++;
81           break;
82
83         case '(':
84         case '<':
85           bracesBalance--;
86           lessPos = i;
87           break;
88
89         case '@':
90         case '.':
91           if (bracesBalance <= 0) break loop;
92           break;
93
94         default:
95           if (Character.isWhitespace(ch) && bracesBalance <= 0) {
96             for (int j = i + 1; j < lessPos; j++) {
97               if (!Character.isWhitespace(referenceText.charAt(j))) break loop;
98             }
99             lessPos = i;
100           }
101       }
102     }
103
104     return referenceText.substring(i + 1, lessPos).trim();
105   }
106
107   @NotNull
108   public static String getPresentableText(@NotNull PsiJavaCodeReferenceElement ref) {
109     String name = ref.getReferenceName();
110     PsiAnnotation[] annotations = PsiTreeUtil.getChildrenOfType(ref, PsiAnnotation.class);
111     return getPresentableText(name, notNull(annotations, PsiAnnotation.EMPTY_ARRAY), ref.getTypeParameters());
112   }
113
114   @NotNull
115   public static String getPresentableText(@Nullable String refName, @NotNull PsiAnnotation[] annotations, @NotNull PsiType[] types) {
116     if (types.length == 0 && annotations.length == 0) {
117       return refName != null ? refName : "";
118     }
119
120     StringBuilder buffer = new StringBuilder();
121     appendAnnotations(buffer, annotations, false);
122     buffer.append(refName);
123     appendTypeArgs(buffer, types, false, true);
124     return buffer.toString();
125   }
126
127   @NotNull
128   public static String getQualifiedClassName(@NotNull String referenceText, boolean removeWhitespace) {
129     if (removeWhitespace) {
130       referenceText = removeWhitespace(referenceText);
131     }
132     if (referenceText.indexOf('<') < 0) return referenceText;
133     final StringBuilder buffer = new StringBuilder(referenceText.length());
134     final char[] chars = referenceText.toCharArray();
135     int gtPos = 0;
136     int count = 0;
137     for (int i = 0; i < chars.length; i++) {
138       final char aChar = chars[i];
139       switch (aChar) {
140         case '<':
141           count++;
142           if (count == 1) buffer.append(new String(chars, gtPos, i - gtPos));
143           break;
144         case '>':
145           count--;
146           gtPos = i + 1;
147           break;
148       }
149     }
150     if (count == 0) {
151       buffer.append(new String(chars, gtPos, chars.length - gtPos));
152     }
153     return buffer.toString();
154   }
155
156   private static final Pattern WHITESPACE_PATTERN = Pattern.compile("(?:\\s)|(?:/\\*.*\\*/)|(?://[^\\n]*)");
157   @NotNull
158   private static String removeWhitespace(@NotNull String referenceText) {
159     boolean needsChange = false;
160     for (int i = 0; i < referenceText.length(); i++) {
161       char c = referenceText.charAt(i);
162       if (c == '/' || Character.isWhitespace(c)) {
163         needsChange = true;
164         break;
165       }
166     }
167     if (!needsChange) return referenceText;
168
169     return WHITESPACE_PATTERN.matcher(referenceText).replaceAll("");
170   }
171
172   /**
173    * Obtains text of all type parameter values in a reference.
174    * They go in left-to-right order: {@code A<List<String>, B<Integer>>} yields
175    * {@code ["List<String>", "B<Integer>"]}. Parameters of the outer reference are ignored:
176    * {@code A<List<String>>.B<Integer>} yields {@code ["Integer"]}
177    *
178    * @param referenceText the text of the reference to calculate type parameters for.
179    * @return the calculated array of type parameters.
180    */
181   @NotNull
182   public static String[] getClassParametersText(@NotNull String referenceText) {
183     if (referenceText.indexOf('<') < 0) return ArrayUtil.EMPTY_STRING_ARRAY;
184     final char[] chars = referenceText.toCharArray();
185     int afterLastDotIndex = 0;
186
187     int level = 0;
188     for (int i = 0; i < chars.length; i++) {
189       char aChar = chars[i];
190       switch (aChar) {
191         case '<':
192           level++;
193           break;
194         case '.':
195           if (level == 0) afterLastDotIndex = i + 1;
196           break;
197         case '>':
198           level--;
199           break;
200       }
201     }
202
203     if (level != 0) return ArrayUtil.EMPTY_STRING_ARRAY;
204
205     int dim = 0;
206     for (int i = afterLastDotIndex; i < chars.length; i++) {
207       char aChar = chars[i];
208       switch (aChar) {
209         case '<':
210           level++;
211           if (level == 1) dim++;
212           break;
213         case ',':
214           if (level == 1) dim++;
215           break;
216         case '>':
217           level--;
218           break;
219       }
220     }
221     if (level != 0 || dim == 0) return ArrayUtil.EMPTY_STRING_ARRAY;
222
223     final String[] result = new String[dim];
224     dim = 0;
225     int ltPos = 0;
226     for (int i = afterLastDotIndex; i < chars.length; i++) {
227       final char aChar = chars[i];
228       switch (aChar) {
229         case '<':
230           level++;
231           if (level == 1) ltPos = i;
232           break;
233         case ',':
234           if (level == 1) {
235             result[dim++] = new String(chars, ltPos + 1, i - ltPos - 1);
236             ltPos = i;
237           }
238           break;
239         case '>':
240           level--;
241           if (level == 0) result[dim++] = new String(chars, ltPos + 1, i - ltPos - 1);
242           break;
243       }
244     }
245
246     return result;
247   }
248
249   public static boolean isSubpackageOf(@NotNull String subpackageName, @NotNull String packageName) {
250     return subpackageName.equals(packageName) ||
251            subpackageName.startsWith(packageName) && subpackageName.charAt(packageName.length()) == '.';
252   }
253
254   public static void appendTypeArgs(@NotNull StringBuilder sb, @NotNull PsiType[] types, boolean canonical, boolean annotated) {
255     if (types.length == 0) return;
256
257     sb.append('<');
258     for (int i = 0; i < types.length; i++) {
259       if (i > 0) {
260         sb.append(canonical ? "," : ", ");
261       }
262
263       PsiType type = types[i];
264       if (canonical) {
265         sb.append(type.getCanonicalText(annotated));
266       }
267       else {
268         sb.append(type.getPresentableText());
269       }
270     }
271     sb.append('>');
272   }
273
274   public static boolean appendAnnotations(@NotNull StringBuilder sb, @NotNull PsiAnnotation[] annotations, boolean canonical) {
275     return appendAnnotations(sb, Arrays.asList(annotations), canonical);
276   }
277
278   public static boolean appendAnnotations(@NotNull StringBuilder sb, @NotNull List<? extends PsiAnnotation> annotations, boolean canonical) {
279     boolean updated = false;
280     for (PsiAnnotation annotation : annotations) {
281       if (canonical) {
282         String name = annotation.getQualifiedName();
283         if (name != null) {
284           sb.append('@').append(name).append(annotation.getParameterList().getText()).append(' ');
285           updated = true;
286         }
287       }
288       else {
289         PsiJavaCodeReferenceElement refElement = annotation.getNameReferenceElement();
290         if (refElement != null) {
291           sb.append('@').append(refElement.getText()).append(' ');
292           updated = true;
293         }
294       }
295     }
296     return updated;
297   }
298
299   public static boolean isValidModuleName(@NotNull String name, @NotNull PsiElement context) {
300     PsiNameHelper helper = getInstance(context.getProject());
301     LanguageLevel level = PsiUtil.getLanguageLevel(context);
302     return StringUtil.split(name, ".", true, false).stream().allMatch(part -> helper.isIdentifier(part, level));
303   }
304 }