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 017package org.jetbrains.jet.checkers; 018 019import com.google.common.base.Predicate; 020import com.google.common.collect.Collections2; 021import com.google.common.collect.HashMultiset; 022import com.google.common.collect.Lists; 023import com.google.common.collect.Multiset; 024import com.intellij.openapi.util.TextRange; 025import com.intellij.psi.PsiElement; 026import com.intellij.psi.PsiErrorElement; 027import com.intellij.psi.PsiFile; 028import com.intellij.psi.util.PsiTreeUtil; 029import com.intellij.util.containers.Stack; 030import org.jetbrains.annotations.NotNull; 031import org.jetbrains.jet.lang.diagnostics.AbstractDiagnosticFactory; 032import org.jetbrains.jet.lang.diagnostics.Diagnostic; 033import org.jetbrains.jet.lang.diagnostics.Severity; 034import org.jetbrains.jet.lang.psi.JetReferenceExpression; 035import org.jetbrains.jet.lang.resolve.AnalyzingUtils; 036import org.jetbrains.jet.lang.resolve.BindingContext; 037 038import java.util.*; 039import java.util.regex.Matcher; 040import java.util.regex.Pattern; 041 042public 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}