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