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 if (ranges1.size() != ranges2.size()) return ranges1.size() - ranges2.size(); 048 for (int i = 0; i < ranges1.size(); 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 0; 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 { 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 { 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 private static List<DiagnosticDescriptor> getSortedDiagnosticDescriptors(Collection<Diagnostic> diagnostics) { 443 List<Diagnostic> list = Lists.newArrayList(diagnostics); 444 Collections.sort(list, DIAGNOSTIC_COMPARATOR); 445 446 List<DiagnosticDescriptor> diagnosticDescriptors = Lists.newArrayList(); 447 DiagnosticDescriptor currentDiagnosticDescriptor = null; 448 for (Diagnostic diagnostic : list) { 449 List<TextRange> textRanges = diagnostic.getTextRanges(); 450 if (!diagnostic.isValid()) continue; 451 452 TextRange textRange = textRanges.get(0); 453 if (currentDiagnosticDescriptor != null && currentDiagnosticDescriptor.equalRange(textRange)) { 454 currentDiagnosticDescriptor.diagnostics.add(diagnostic); 455 } 456 else { 457 currentDiagnosticDescriptor = new DiagnosticDescriptor(textRange.getStartOffset(), textRange.getEndOffset(), diagnostic); 458 diagnosticDescriptors.add(currentDiagnosticDescriptor); 459 } 460 } 461 return diagnosticDescriptors; 462 } 463 464 private static class DiagnosticDescriptor { 465 private final int start; 466 private final int end; 467 private final List<Diagnostic> diagnostics = Lists.newArrayList(); 468 469 DiagnosticDescriptor(int start, int end, Diagnostic diagnostic) { 470 this.start = start; 471 this.end = end; 472 this.diagnostics.add(diagnostic); 473 } 474 475 public boolean equalRange(TextRange textRange) { 476 return start == textRange.getStartOffset() && end == textRange.getEndOffset(); 477 } 478 479 public Multiset<String> getDiagnosticTypeStrings() { 480 Multiset<String> actualDiagnosticTypes = HashMultiset.create(); 481 for (Diagnostic diagnostic : diagnostics) { 482 actualDiagnosticTypes.add(diagnostic.getFactory().getName()); 483 } 484 return actualDiagnosticTypes; 485 } 486 487 public int getStart() { 488 return start; 489 } 490 491 public int getEnd() { 492 return end; 493 } 494 495 public List<Diagnostic> getDiagnostics() { 496 return diagnostics; 497 } 498 499 public TextRange getTextRange() { 500 return new TextRange(start, end); 501 } 502 } 503 504 public static class DiagnosedRange { 505 private final int start; 506 private int end; 507 private final Multiset<String> diagnostics = HashMultiset.create(); 508 private PsiFile file; 509 510 private DiagnosedRange(int start) { 511 this.start = start; 512 } 513 514 public int getStart() { 515 return start; 516 } 517 518 public int getEnd() { 519 return end; 520 } 521 522 public Multiset<String> getDiagnostics() { 523 return diagnostics; 524 } 525 526 public void setEnd(int end) { 527 this.end = end; 528 } 529 530 public void addDiagnostic(String diagnostic) { 531 diagnostics.add(diagnostic); 532 } 533 534 public void setFile(@NotNull PsiFile file) { 535 this.file = file; 536 } 537 538 @NotNull 539 public PsiFile getFile() { 540 return file; 541 } 542 } 543 }