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