001 /*
002 * Copyright 2010-2013 JetBrains s.r.o.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017 package org.jetbrains.jet.checkers;
018
019 import com.google.common.base.Predicate;
020 import com.google.common.collect.*;
021 import com.intellij.openapi.util.TextRange;
022 import com.intellij.psi.PsiElement;
023 import com.intellij.psi.PsiErrorElement;
024 import com.intellij.psi.PsiFile;
025 import com.intellij.psi.util.PsiTreeUtil;
026 import com.intellij.util.Function;
027 import com.intellij.util.containers.Stack;
028 import org.jetbrains.annotations.NotNull;
029 import org.jetbrains.jet.lang.diagnostics.Diagnostic;
030 import org.jetbrains.jet.lang.diagnostics.DiagnosticFactory;
031 import org.jetbrains.jet.lang.diagnostics.Severity;
032 import org.jetbrains.jet.lang.psi.JetExpression;
033 import org.jetbrains.jet.lang.psi.JetReferenceExpression;
034 import org.jetbrains.jet.lang.resolve.AnalyzingUtils;
035 import org.jetbrains.jet.lang.resolve.BindingContext;
036
037 import java.util.*;
038 import java.util.regex.Matcher;
039 import java.util.regex.Pattern;
040
041 public class CheckerTestUtil {
042 public static final Comparator<Diagnostic> DIAGNOSTIC_COMPARATOR = new Comparator<Diagnostic>() {
043 @Override
044 public int compare(@NotNull Diagnostic o1, @NotNull Diagnostic o2) {
045 List<TextRange> ranges1 = o1.getTextRanges();
046 List<TextRange> ranges2 = o2.getTextRanges();
047 int minNumberOfRanges = ranges1.size() < ranges2.size() ? ranges1.size() : ranges2.size();
048 for (int i = 0; i < minNumberOfRanges; i++) {
049 TextRange range1 = ranges1.get(i);
050 TextRange range2 = ranges2.get(i);
051 int startOffset1 = range1.getStartOffset();
052 int startOffset2 = range2.getStartOffset();
053 if (startOffset1 != startOffset2) {
054 // Start early -- go first
055 return startOffset1 - range2.getStartOffset();
056 }
057 int endOffset1 = range1.getEndOffset();
058 int endOffset2 = range2.getEndOffset();
059 if (endOffset1 != endOffset2) {
060 // start at the same offset, the one who end later is the outer, i.e. goes first
061 return endOffset2 - endOffset1;
062 }
063 }
064 return ranges1.size() - ranges2.size();
065 }
066 };
067 private static final Pattern RANGE_START_OR_END_PATTERN = Pattern.compile("(<!\\w+(,\\s*\\w+)*!>)|(<!>)");
068 private static final Pattern INDIVIDUAL_DIAGNOSTIC_PATTERN = Pattern.compile("\\w+");
069
070 public static List<Diagnostic> getDiagnosticsIncludingSyntaxErrors(BindingContext bindingContext, final PsiElement root) {
071 ArrayList<Diagnostic> diagnostics = new ArrayList<Diagnostic>();
072 diagnostics.addAll(Collections2.filter(bindingContext.getDiagnostics().all(),
073 new Predicate<Diagnostic>() {
074 @Override
075 public boolean apply(Diagnostic diagnostic) {
076 return PsiTreeUtil.isAncestor(root, diagnostic.getPsiElement(), false);
077 }
078 }));
079 for (PsiErrorElement errorElement : AnalyzingUtils.getSyntaxErrorRanges(root)) {
080 diagnostics.add(new SyntaxErrorDiagnostic(errorElement));
081 }
082 List<Diagnostic> debugAnnotations = getDebugInfoDiagnostics(root, bindingContext);
083 diagnostics.addAll(debugAnnotations);
084 return diagnostics;
085 }
086
087 public static List<Diagnostic> getDebugInfoDiagnostics(@NotNull PsiElement root, @NotNull BindingContext bindingContext) {
088 final List<Diagnostic> debugAnnotations = Lists.newArrayList();
089 DebugInfoUtil.markDebugAnnotations(root, bindingContext, new DebugInfoUtil.DebugInfoReporter() {
090 @Override
091 public void reportElementWithErrorType(@NotNull JetReferenceExpression expression) {
092 newDiagnostic(expression, DebugInfoDiagnosticFactory.ELEMENT_WITH_ERROR_TYPE);
093 }
094
095 @Override
096 public void reportMissingUnresolved(@NotNull JetReferenceExpression expression) {
097 newDiagnostic(expression, DebugInfoDiagnosticFactory.MISSING_UNRESOLVED);
098 }
099
100 @Override
101 public void reportUnresolvedWithTarget(@NotNull JetReferenceExpression expression, @NotNull String target) {
102 newDiagnostic(expression, DebugInfoDiagnosticFactory.UNRESOLVED_WITH_TARGET);
103 }
104
105 private void newDiagnostic(JetReferenceExpression expression, DebugInfoDiagnosticFactory factory) {
106 debugAnnotations.add(new DebugInfoDiagnostic(expression, factory));
107 }
108 });
109 // this code is used in tests and in internal action 'copy current file as diagnostic test'
110 //noinspection TestOnlyProblems
111 for (JetExpression expression : bindingContext.getSliceContents(BindingContext.SMARTCAST).keySet()) {
112 if (PsiTreeUtil.isAncestor(root, expression, false)) {
113 debugAnnotations.add(new DebugInfoDiagnostic(expression, DebugInfoDiagnosticFactory.SMARTCAST));
114 }
115 }
116 return debugAnnotations;
117 }
118
119 public interface DiagnosticDiffCallbacks {
120 void missingDiagnostic(String type, int expectedStart, int expectedEnd);
121 void unexpectedDiagnostic(String type, int actualStart, int actualEnd);
122 }
123
124 public static void diagnosticsDiff(
125 List<DiagnosedRange> expected,
126 Collection<Diagnostic> actual,
127 DiagnosticDiffCallbacks callbacks
128 ) {
129 assertSameFile(actual);
130
131 Iterator<DiagnosedRange> expectedDiagnostics = expected.iterator();
132 List<DiagnosticDescriptor> sortedDiagnosticDescriptors = getSortedDiagnosticDescriptors(actual);
133 Iterator<DiagnosticDescriptor> actualDiagnostics = sortedDiagnosticDescriptors.iterator();
134
135 DiagnosedRange currentExpected = safeAdvance(expectedDiagnostics);
136 DiagnosticDescriptor currentActual = safeAdvance(actualDiagnostics);
137 while (currentExpected != null || currentActual != null) {
138 if (currentExpected != null) {
139 if (currentActual == null) {
140 missingDiagnostics(callbacks, currentExpected);
141 currentExpected = safeAdvance(expectedDiagnostics);
142 }
143 else {
144 int expectedStart = currentExpected.getStart();
145 int actualStart = currentActual.getStart();
146 int expectedEnd = currentExpected.getEnd();
147 int actualEnd = currentActual.getEnd();
148 if (expectedStart < actualStart) {
149 missingDiagnostics(callbacks, currentExpected);
150 currentExpected = safeAdvance(expectedDiagnostics);
151 }
152 else if (expectedStart > actualStart) {
153 unexpectedDiagnostics(currentActual.getDiagnostics(), callbacks);
154 currentActual = safeAdvance(actualDiagnostics);
155 }
156 else if (expectedEnd > actualEnd) {
157 assert expectedStart == actualStart;
158 missingDiagnostics(callbacks, currentExpected);
159 currentExpected = safeAdvance(expectedDiagnostics);
160 }
161 else if (expectedEnd < actualEnd) {
162 assert expectedStart == actualStart;
163 unexpectedDiagnostics(currentActual.getDiagnostics(), callbacks);
164 currentActual = safeAdvance(actualDiagnostics);
165 }
166 else {
167 assert expectedStart == actualStart && expectedEnd == actualEnd;
168 Multiset<String> actualDiagnosticTypes = currentActual.getDiagnosticTypeStrings();
169 Multiset<String> expectedDiagnosticTypes = currentExpected.getDiagnostics();
170 if (!actualDiagnosticTypes.equals(expectedDiagnosticTypes)) {
171 Multiset<String> notInActualTypes = HashMultiset.create(expectedDiagnosticTypes);
172 Multisets.removeOccurrences(notInActualTypes, actualDiagnosticTypes);
173
174 Multiset<String> notInExpectedTypes = HashMultiset.create(actualDiagnosticTypes);
175 Multisets.removeOccurrences(notInExpectedTypes, expectedDiagnosticTypes);
176
177 for (String type : notInActualTypes) {
178 callbacks.missingDiagnostic(type, expectedStart, expectedEnd);
179 }
180 for (String type : notInExpectedTypes) {
181 callbacks.unexpectedDiagnostic(type, actualStart, actualEnd);
182 }
183 }
184 currentExpected = safeAdvance(expectedDiagnostics);
185 currentActual = safeAdvance(actualDiagnostics);
186 }
187 }
188 }
189 else {
190 //noinspection ConstantConditions
191 assert (currentActual != null);
192
193 unexpectedDiagnostics(currentActual.getDiagnostics(), callbacks);
194 currentActual = safeAdvance(actualDiagnostics);
195 }
196 }
197 }
198
199 private static void assertSameFile(Collection<Diagnostic> actual) {
200 if (actual.isEmpty()) return;
201 PsiFile file = actual.iterator().next().getPsiElement().getContainingFile();
202 for (Diagnostic diagnostic : actual) {
203 assert diagnostic.getPsiFile().equals(file)
204 : "All diagnostics should come from the same file: " + diagnostic.getPsiFile() + ", " + file;
205 }
206 }
207
208 private static void unexpectedDiagnostics(List<Diagnostic> actual, DiagnosticDiffCallbacks callbacks) {
209 for (Diagnostic diagnostic : actual) {
210 List<TextRange> textRanges = diagnostic.getTextRanges();
211 for (TextRange textRange : textRanges) {
212 callbacks.unexpectedDiagnostic(diagnostic.getFactory().getName(), textRange.getStartOffset(), textRange.getEndOffset());
213 }
214 }
215 }
216
217 private static void missingDiagnostics(DiagnosticDiffCallbacks callbacks, DiagnosedRange currentExpected) {
218 for (String type : currentExpected.getDiagnostics()) {
219 callbacks.missingDiagnostic(type, currentExpected.getStart(), currentExpected.getEnd());
220 }
221 }
222
223 private static <T> T safeAdvance(Iterator<T> iterator) {
224 return iterator.hasNext() ? iterator.next() : null;
225 }
226
227 public static String parseDiagnosedRanges(String text, List<DiagnosedRange> result) {
228 Matcher matcher = RANGE_START_OR_END_PATTERN.matcher(text);
229
230 Stack<DiagnosedRange> opened = new Stack<DiagnosedRange>();
231
232 int offsetCompensation = 0;
233
234 while (matcher.find()) {
235 int effectiveOffset = matcher.start() - offsetCompensation;
236 String matchedText = matcher.group();
237 if ("<!>".equals(matchedText)) {
238 opened.pop().setEnd(effectiveOffset);
239 }
240 else {
241 Matcher diagnosticTypeMatcher = INDIVIDUAL_DIAGNOSTIC_PATTERN.matcher(matchedText);
242 DiagnosedRange range = new DiagnosedRange(effectiveOffset);
243 while (diagnosticTypeMatcher.find()) {
244 range.addDiagnostic(diagnosticTypeMatcher.group());
245 }
246 opened.push(range);
247 result.add(range);
248 }
249 offsetCompensation += matchedText.length();
250 }
251
252 assert opened.isEmpty() : "Stack is not empty";
253
254 matcher.reset();
255 return matcher.replaceAll("");
256 }
257
258 public static StringBuffer addDiagnosticMarkersToText(@NotNull final PsiFile psiFile, @NotNull Collection<Diagnostic> diagnostics) {
259 return addDiagnosticMarkersToText(psiFile, diagnostics, new Function<PsiFile, String>() {
260 @Override
261 public String fun(PsiFile file) {
262 return file.getText();
263 }
264 });
265 }
266
267 public static StringBuffer addDiagnosticMarkersToText(
268 @NotNull final PsiFile psiFile,
269 @NotNull Collection<Diagnostic> diagnostics,
270 @NotNull Function<PsiFile, String> getFileText
271 ) {
272 String text = getFileText.fun(psiFile);
273 StringBuffer result = new StringBuffer();
274 diagnostics = Collections2.filter(diagnostics, new Predicate<Diagnostic>() {
275 @Override
276 public boolean apply(Diagnostic diagnostic) {
277 return psiFile.equals(diagnostic.getPsiFile());
278 }
279 });
280 if (!diagnostics.isEmpty()) {
281 List<DiagnosticDescriptor> diagnosticDescriptors = getSortedDiagnosticDescriptors(diagnostics);
282
283 Stack<DiagnosticDescriptor> opened = new Stack<DiagnosticDescriptor>();
284 ListIterator<DiagnosticDescriptor> iterator = diagnosticDescriptors.listIterator();
285 DiagnosticDescriptor currentDescriptor = iterator.next();
286
287 for (int i = 0; i < text.length(); i++) {
288 char c = text.charAt(i);
289 while (!opened.isEmpty() && i == opened.peek().end) {
290 closeDiagnosticString(result);
291 opened.pop();
292 }
293 while (currentDescriptor != null && i == currentDescriptor.start) {
294 openDiagnosticsString(result, currentDescriptor);
295 if (currentDescriptor.getEnd() == i) {
296 closeDiagnosticString(result);
297 }
298 else {
299 opened.push(currentDescriptor);
300 }
301 if (iterator.hasNext()) {
302 currentDescriptor = iterator.next();
303 }
304 else {
305 currentDescriptor = null;
306 }
307 }
308 result.append(c);
309 }
310
311 if (currentDescriptor != null) {
312 assert currentDescriptor.start == text.length();
313 assert currentDescriptor.end == text.length();
314 openDiagnosticsString(result, currentDescriptor);
315 opened.push(currentDescriptor);
316 }
317
318 while (!opened.isEmpty() && text.length() == opened.peek().end) {
319 closeDiagnosticString(result);
320 opened.pop();
321 }
322
323 assert opened.isEmpty() : "Stack is not empty: " + opened;
324
325 }
326 else {
327 result.append(text);
328 }
329 return result;
330 }
331
332 private static void openDiagnosticsString(StringBuffer result, DiagnosticDescriptor currentDescriptor) {
333 result.append("<!");
334 for (Iterator<Diagnostic> iterator = currentDescriptor.diagnostics.iterator(); iterator.hasNext(); ) {
335 Diagnostic diagnostic = iterator.next();
336 result.append(diagnostic.getFactory().getName());
337 if (iterator.hasNext()) {
338 result.append(", ");
339 }
340 }
341 result.append("!>");
342 }
343
344 private static void closeDiagnosticString(StringBuffer result) {
345 result.append("<!>");
346 }
347
348 public static class AbstractDiagnosticForTests implements Diagnostic {
349 private final PsiElement element;
350 private final DiagnosticFactory<?> factory;
351
352 public AbstractDiagnosticForTests(@NotNull PsiElement element, @NotNull DiagnosticFactory<?> factory) {
353 this.element = element;
354 this.factory = factory;
355 }
356
357 @NotNull
358 @Override
359 public DiagnosticFactory<?> getFactory() {
360 return factory;
361 }
362
363 @NotNull
364 @Override
365 public Severity getSeverity() {
366 return Severity.ERROR;
367 }
368
369 @NotNull
370 @Override
371 public PsiElement getPsiElement() {
372 return element;
373 }
374
375 @NotNull
376 @Override
377 public List<TextRange> getTextRanges() {
378 return Collections.singletonList(element.getTextRange());
379 }
380
381 @NotNull
382 @Override
383 public PsiFile getPsiFile() {
384 return element.getContainingFile();
385 }
386
387 @Override
388 public boolean isValid() {
389 return true;
390 }
391 }
392
393 public static class SyntaxErrorDiagnosticFactory extends DiagnosticFactory<SyntaxErrorDiagnostic> {
394 public static final SyntaxErrorDiagnosticFactory INSTANCE = new SyntaxErrorDiagnosticFactory();
395
396 private SyntaxErrorDiagnosticFactory() {
397 super(Severity.ERROR);
398 }
399
400 @NotNull
401 @Override
402 public String getName() {
403 return "SYNTAX";
404 }
405 }
406
407 public static class SyntaxErrorDiagnostic extends AbstractDiagnosticForTests {
408 public SyntaxErrorDiagnostic(@NotNull PsiErrorElement errorElement) {
409 super(errorElement, SyntaxErrorDiagnosticFactory.INSTANCE);
410 }
411 }
412
413 public static class DebugInfoDiagnosticFactory extends DiagnosticFactory<DebugInfoDiagnostic> {
414 public static final DebugInfoDiagnosticFactory SMARTCAST = new DebugInfoDiagnosticFactory("SMARTCAST");
415 public static final DebugInfoDiagnosticFactory ELEMENT_WITH_ERROR_TYPE = new DebugInfoDiagnosticFactory("ELEMENT_WITH_ERROR_TYPE");
416 public static final DebugInfoDiagnosticFactory UNRESOLVED_WITH_TARGET = new DebugInfoDiagnosticFactory("UNRESOLVED_WITH_TARGET");
417 public static final DebugInfoDiagnosticFactory MISSING_UNRESOLVED = new DebugInfoDiagnosticFactory("MISSING_UNRESOLVED");
418
419 private final String name;
420 private DebugInfoDiagnosticFactory(String name, Severity severity) {
421 super(severity);
422 this.name = name;
423 }
424
425 private DebugInfoDiagnosticFactory(String name) {
426 this(name, Severity.ERROR);
427 }
428
429 @NotNull
430 @Override
431 public String getName() {
432 return "DEBUG_INFO_" + name;
433 }
434 }
435
436 public static class DebugInfoDiagnostic extends AbstractDiagnosticForTests {
437 public DebugInfoDiagnostic(@NotNull JetExpression reference, @NotNull DebugInfoDiagnosticFactory factory) {
438 super(reference, factory);
439 }
440 }
441
442 @NotNull
443 private static List<DiagnosticDescriptor> getSortedDiagnosticDescriptors(@NotNull Collection<Diagnostic> diagnostics) {
444 LinkedListMultimap<TextRange, Diagnostic> diagnosticsGroupedByRanges = LinkedListMultimap.create();
445 for (Diagnostic diagnostic : diagnostics) {
446 if (!diagnostic.isValid()) continue;
447 for (TextRange textRange : diagnostic.getTextRanges()) {
448 diagnosticsGroupedByRanges.put(textRange, diagnostic);
449 }
450 }
451 List<DiagnosticDescriptor> diagnosticDescriptors = Lists.newArrayList();
452 for (TextRange range : diagnosticsGroupedByRanges.keySet()) {
453 diagnosticDescriptors.add(
454 new DiagnosticDescriptor(range.getStartOffset(), range.getEndOffset(), diagnosticsGroupedByRanges.get(range)));
455 }
456 Collections.sort(diagnosticDescriptors, new Comparator<DiagnosticDescriptor>() {
457 @Override
458 public int compare(DiagnosticDescriptor d1, DiagnosticDescriptor d2) {
459 // Start early -- go first; start at the same offset, the one who end later is the outer, i.e. goes first
460 return (d1.start != d2.start) ? d1.start - d2.start : d2.end - d1.end;
461 }
462 });
463 return diagnosticDescriptors;
464 }
465
466 private static class DiagnosticDescriptor {
467 private final int start;
468 private final int end;
469 private final List<Diagnostic> diagnostics;
470
471 DiagnosticDescriptor(int start, int end, List<Diagnostic> diagnostics) {
472 this.start = start;
473 this.end = end;
474 this.diagnostics = diagnostics;
475 }
476
477 public Multiset<String> getDiagnosticTypeStrings() {
478 Multiset<String> actualDiagnosticTypes = HashMultiset.create();
479 for (Diagnostic diagnostic : diagnostics) {
480 actualDiagnosticTypes.add(diagnostic.getFactory().getName());
481 }
482 return actualDiagnosticTypes;
483 }
484
485 public int getStart() {
486 return start;
487 }
488
489 public int getEnd() {
490 return end;
491 }
492
493 public List<Diagnostic> getDiagnostics() {
494 return diagnostics;
495 }
496
497 public TextRange getTextRange() {
498 return new TextRange(start, end);
499 }
500 }
501
502 public static class DiagnosedRange {
503 private final int start;
504 private int end;
505 private final Multiset<String> diagnostics = HashMultiset.create();
506 private PsiFile file;
507
508 private DiagnosedRange(int start) {
509 this.start = start;
510 }
511
512 public int getStart() {
513 return start;
514 }
515
516 public int getEnd() {
517 return end;
518 }
519
520 public Multiset<String> getDiagnostics() {
521 return diagnostics;
522 }
523
524 public void setEnd(int end) {
525 this.end = end;
526 }
527
528 public void addDiagnostic(String diagnostic) {
529 diagnostics.add(diagnostic);
530 }
531
532 public void setFile(@NotNull PsiFile file) {
533 this.file = file;
534 }
535
536 @NotNull
537 public PsiFile getFile() {
538 return file;
539 }
540 }
541 }