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.Collections2; 021 import com.google.common.collect.HashMultiset; 022 import com.google.common.collect.Lists; 023 import com.google.common.collect.Multiset; 024 import com.intellij.openapi.util.TextRange; 025 import com.intellij.psi.PsiElement; 026 import com.intellij.psi.PsiErrorElement; 027 import com.intellij.psi.PsiFile; 028 import com.intellij.psi.util.PsiTreeUtil; 029 import com.intellij.util.containers.Stack; 030 import org.jetbrains.annotations.NotNull; 031 import org.jetbrains.jet.lang.diagnostics.DiagnosticFactory; 032 import org.jetbrains.jet.lang.diagnostics.Diagnostic; 033 import org.jetbrains.jet.lang.diagnostics.Severity; 034 import org.jetbrains.jet.lang.psi.JetReferenceExpression; 035 import org.jetbrains.jet.lang.resolve.AnalyzingUtils; 036 import org.jetbrains.jet.lang.resolve.BindingContext; 037 038 import java.util.*; 039 import java.util.regex.Matcher; 040 import java.util.regex.Pattern; 041 042 public 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().all(), 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 DiagnosticFactory factory; 333 334 public AbstractDiagnosticForTests(@NotNull PsiElement element, @NotNull DiagnosticFactory factory) { 335 this.element = element; 336 this.factory = factory; 337 } 338 339 @NotNull 340 @Override 341 public DiagnosticFactory getFactory() { 342 return factory; 343 } 344 345 @NotNull 346 @Override 347 public Severity getSeverity() { 348 return Severity.ERROR; 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 public static class SyntaxErrorDiagnosticFactory extends DiagnosticFactory { 376 public static final SyntaxErrorDiagnosticFactory INSTANCE = new SyntaxErrorDiagnosticFactory(); 377 378 private SyntaxErrorDiagnosticFactory() { 379 super(Severity.ERROR); 380 } 381 382 @NotNull 383 @Override 384 public String getName() { 385 return "SYNTAX"; 386 } 387 } 388 389 public static class SyntaxErrorDiagnostic extends AbstractDiagnosticForTests { 390 public SyntaxErrorDiagnostic(@NotNull PsiErrorElement errorElement) { 391 super(errorElement, SyntaxErrorDiagnosticFactory.INSTANCE); 392 } 393 } 394 395 public static class DebugInfoDiagnosticFactory extends DiagnosticFactory { 396 public static final DebugInfoDiagnosticFactory ELEMENT_WITH_ERROR_TYPE = new DebugInfoDiagnosticFactory("ELEMENT_WITH_ERROR_TYPE"); 397 public static final DebugInfoDiagnosticFactory UNRESOLVED_WITH_TARGET = new DebugInfoDiagnosticFactory("UNRESOLVED_WITH_TARGET"); 398 public static final DebugInfoDiagnosticFactory MISSING_UNRESOLVED = new DebugInfoDiagnosticFactory("MISSING_UNRESOLVED"); 399 400 private final String name; 401 private DebugInfoDiagnosticFactory(String name) { 402 super(Severity.ERROR); 403 this.name = name; 404 } 405 406 @NotNull 407 @Override 408 public String getName() { 409 return "DEBUG_INFO_" + name; 410 } 411 } 412 413 public static class DebugInfoDiagnostic extends AbstractDiagnosticForTests { 414 public DebugInfoDiagnostic(@NotNull JetReferenceExpression reference, @NotNull DebugInfoDiagnosticFactory factory) { 415 super(reference, factory); 416 } 417 } 418 419 private static List<DiagnosticDescriptor> getSortedDiagnosticDescriptors(Collection<Diagnostic> diagnostics) { 420 List<Diagnostic> list = Lists.newArrayList(diagnostics); 421 Collections.sort(list, DIAGNOSTIC_COMPARATOR); 422 423 List<DiagnosticDescriptor> diagnosticDescriptors = Lists.newArrayList(); 424 DiagnosticDescriptor currentDiagnosticDescriptor = null; 425 for (Diagnostic diagnostic : list) { 426 List<TextRange> textRanges = diagnostic.getTextRanges(); 427 if (!diagnostic.isValid()) continue; 428 429 TextRange textRange = textRanges.get(0); 430 if (currentDiagnosticDescriptor != null && currentDiagnosticDescriptor.equalRange(textRange)) { 431 currentDiagnosticDescriptor.diagnostics.add(diagnostic); 432 } 433 else { 434 currentDiagnosticDescriptor = new DiagnosticDescriptor(textRange.getStartOffset(), textRange.getEndOffset(), diagnostic); 435 diagnosticDescriptors.add(currentDiagnosticDescriptor); 436 } 437 } 438 return diagnosticDescriptors; 439 } 440 441 private static class DiagnosticDescriptor { 442 private final int start; 443 private final int end; 444 private final List<Diagnostic> diagnostics = Lists.newArrayList(); 445 446 DiagnosticDescriptor(int start, int end, Diagnostic diagnostic) { 447 this.start = start; 448 this.end = end; 449 this.diagnostics.add(diagnostic); 450 } 451 452 public boolean equalRange(TextRange textRange) { 453 return start == textRange.getStartOffset() && end == textRange.getEndOffset(); 454 } 455 456 public Multiset<String> getDiagnosticTypeStrings() { 457 Multiset<String> actualDiagnosticTypes = HashMultiset.create(); 458 for (Diagnostic diagnostic : diagnostics) { 459 actualDiagnosticTypes.add(diagnostic.getFactory().getName()); 460 } 461 return actualDiagnosticTypes; 462 } 463 464 public int getStart() { 465 return start; 466 } 467 468 public int getEnd() { 469 return end; 470 } 471 472 public List<Diagnostic> getDiagnostics() { 473 return diagnostics; 474 } 475 476 public TextRange getTextRange() { 477 return new TextRange(start, end); 478 } 479 } 480 481 public static class DiagnosedRange { 482 private final int start; 483 private int end; 484 private final Multiset<String> diagnostics = HashMultiset.create(); 485 private PsiFile file; 486 487 private DiagnosedRange(int start) { 488 this.start = start; 489 } 490 491 public int getStart() { 492 return start; 493 } 494 495 public int getEnd() { 496 return end; 497 } 498 499 public Multiset<String> getDiagnostics() { 500 return diagnostics; 501 } 502 503 public void setEnd(int end) { 504 this.end = end; 505 } 506 507 public void addDiagnostic(String diagnostic) { 508 diagnostics.add(diagnostic); 509 } 510 511 public void setFile(@NotNull PsiFile file) { 512 this.file = file; 513 } 514 515 @NotNull 516 public PsiFile getFile() { 517 return file; 518 } 519 } 520 }