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 }