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.AUTOCAST).keySet()) { 112 if (PsiTreeUtil.isAncestor(root, expression, false)) { 113 debugAnnotations.add(new DebugInfoDiagnostic(expression, DebugInfoDiagnosticFactory.AUTOCAST)); 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 AUTOCAST = new DebugInfoDiagnosticFactory("AUTOCAST"); 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 }