001 /* 002 * Copyright 2010-2015 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.kotlin.checkers; 018 019 import com.google.common.base.Predicate; 020 import com.google.common.collect.Collections2; 021 import com.google.common.collect.LinkedListMultimap; 022 import com.google.common.collect.Lists; 023 import com.intellij.openapi.util.TextRange; 024 import com.intellij.openapi.util.text.StringUtil; 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.Function; 030 import com.intellij.util.SmartList; 031 import com.intellij.util.containers.ContainerUtil; 032 import com.intellij.util.containers.Stack; 033 import org.jetbrains.annotations.NotNull; 034 import org.jetbrains.annotations.Nullable; 035 import org.jetbrains.kotlin.descriptors.DeclarationDescriptor; 036 import org.jetbrains.kotlin.diagnostics.Diagnostic; 037 import org.jetbrains.kotlin.diagnostics.DiagnosticFactory; 038 import org.jetbrains.kotlin.diagnostics.Severity; 039 import org.jetbrains.kotlin.diagnostics.rendering.AbstractDiagnosticWithParametersRenderer; 040 import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages; 041 import org.jetbrains.kotlin.diagnostics.rendering.DiagnosticRenderer; 042 import org.jetbrains.kotlin.psi.JetElement; 043 import org.jetbrains.kotlin.psi.JetExpression; 044 import org.jetbrains.kotlin.psi.JetReferenceExpression; 045 import org.jetbrains.kotlin.resolve.AnalyzingUtils; 046 import org.jetbrains.kotlin.resolve.BindingContext; 047 048 import java.util.*; 049 import java.util.regex.Matcher; 050 import java.util.regex.Pattern; 051 052 public class CheckerTestUtil { 053 public static final Comparator<Diagnostic> DIAGNOSTIC_COMPARATOR = new Comparator<Diagnostic>() { 054 @Override 055 public int compare(@NotNull Diagnostic o1, @NotNull Diagnostic o2) { 056 List<TextRange> ranges1 = o1.getTextRanges(); 057 List<TextRange> ranges2 = o2.getTextRanges(); 058 int minNumberOfRanges = ranges1.size() < ranges2.size() ? ranges1.size() : ranges2.size(); 059 for (int i = 0; i < minNumberOfRanges; i++) { 060 TextRange range1 = ranges1.get(i); 061 TextRange range2 = ranges2.get(i); 062 int startOffset1 = range1.getStartOffset(); 063 int startOffset2 = range2.getStartOffset(); 064 if (startOffset1 != startOffset2) { 065 // Start early -- go first 066 return startOffset1 - range2.getStartOffset(); 067 } 068 int endOffset1 = range1.getEndOffset(); 069 int endOffset2 = range2.getEndOffset(); 070 if (endOffset1 != endOffset2) { 071 // start at the same offset, the one who end later is the outer, i.e. goes first 072 return endOffset2 - endOffset1; 073 } 074 } 075 return ranges1.size() - ranges2.size(); 076 } 077 }; 078 079 private static final String IGNORE_DIAGNOSTIC_PARAMETER = "IGNORE"; 080 private static final String SHOULD_BE_ESCAPED = "\\)\\(;"; 081 private static final String DIAGNOSTIC_PARAMETER = "(?:(?:\\\\[" + SHOULD_BE_ESCAPED + "])|[^" + SHOULD_BE_ESCAPED + "])+"; 082 private static final String INDIVIDUAL_DIAGNOSTIC = "(\\w+)(\\(" + DIAGNOSTIC_PARAMETER + "(;\\s*" + DIAGNOSTIC_PARAMETER + ")*\\))?"; 083 private static final Pattern RANGE_START_OR_END_PATTERN = Pattern.compile("(<!"+ 084 INDIVIDUAL_DIAGNOSTIC +"(,\\s*"+ 085 INDIVIDUAL_DIAGNOSTIC +")*!>)|(<!>)"); 086 private static final Pattern INDIVIDUAL_DIAGNOSTIC_PATTERN = Pattern.compile(INDIVIDUAL_DIAGNOSTIC); 087 private static final Pattern INDIVIDUAL_PARAMETER_PATTERN = Pattern.compile(DIAGNOSTIC_PARAMETER); 088 089 @NotNull 090 public static List<Diagnostic> getDiagnosticsIncludingSyntaxErrors( 091 @NotNull BindingContext bindingContext, 092 @NotNull final PsiElement root, 093 boolean markDynamicCalls, 094 @Nullable List<DeclarationDescriptor> dynamicCallDescriptors 095 ) { 096 List<Diagnostic> diagnostics = new ArrayList<Diagnostic>(); 097 diagnostics.addAll(Collections2.filter(bindingContext.getDiagnostics().all(), 098 new Predicate<Diagnostic>() { 099 @Override 100 public boolean apply(Diagnostic diagnostic) { 101 return PsiTreeUtil.isAncestor(root, diagnostic.getPsiElement(), false); 102 } 103 })); 104 for (PsiErrorElement errorElement : AnalyzingUtils.getSyntaxErrorRanges(root)) { 105 diagnostics.add(new SyntaxErrorDiagnostic(errorElement)); 106 } 107 List<Diagnostic> debugAnnotations = getDebugInfoDiagnostics(root, bindingContext, markDynamicCalls, dynamicCallDescriptors); 108 diagnostics.addAll(debugAnnotations); 109 return diagnostics; 110 } 111 112 @NotNull 113 private static List<Diagnostic> getDebugInfoDiagnostics( 114 @NotNull PsiElement root, 115 @NotNull BindingContext bindingContext, 116 final boolean markDynamicCalls, 117 @Nullable final List<DeclarationDescriptor> dynamicCallDescriptors 118 ) { 119 final List<Diagnostic> debugAnnotations = Lists.newArrayList(); 120 DebugInfoUtil.markDebugAnnotations(root, bindingContext, new DebugInfoUtil.DebugInfoReporter() { 121 @Override 122 public void reportElementWithErrorType(@NotNull JetReferenceExpression expression) { 123 newDiagnostic(expression, DebugInfoDiagnosticFactory.ELEMENT_WITH_ERROR_TYPE); 124 } 125 126 @Override 127 public void reportMissingUnresolved(@NotNull JetReferenceExpression expression) { 128 newDiagnostic(expression, DebugInfoDiagnosticFactory.MISSING_UNRESOLVED); 129 } 130 131 @Override 132 public void reportUnresolvedWithTarget(@NotNull JetReferenceExpression expression, @NotNull String target) { 133 newDiagnostic(expression, DebugInfoDiagnosticFactory.UNRESOLVED_WITH_TARGET); 134 } 135 136 @Override 137 public void reportDynamicCall(@NotNull JetElement element, DeclarationDescriptor declarationDescriptor) { 138 if (dynamicCallDescriptors != null) { 139 dynamicCallDescriptors.add(declarationDescriptor); 140 } 141 142 if (markDynamicCalls) { 143 newDiagnostic(element, DebugInfoDiagnosticFactory.DYNAMIC); 144 } 145 } 146 147 private void newDiagnostic(JetElement element, DebugInfoDiagnosticFactory factory) { 148 debugAnnotations.add(new DebugInfoDiagnostic(element, factory)); 149 } 150 }); 151 // this code is used in tests and in internal action 'copy current file as diagnostic test' 152 //noinspection TestOnlyProblems 153 for (JetExpression expression : bindingContext.getSliceContents(BindingContext.SMARTCAST).keySet()) { 154 if (PsiTreeUtil.isAncestor(root, expression, false)) { 155 debugAnnotations.add(new DebugInfoDiagnostic(expression, DebugInfoDiagnosticFactory.SMARTCAST)); 156 } 157 } 158 return debugAnnotations; 159 } 160 161 public interface DiagnosticDiffCallbacks { 162 void missingDiagnostic(TextDiagnostic diagnostic, int expectedStart, int expectedEnd); 163 void wrongParametersDiagnostic(TextDiagnostic expectedDiagnostic, TextDiagnostic actualDiagnostic, int start, int end); 164 void unexpectedDiagnostic(TextDiagnostic diagnostic, int actualStart, int actualEnd); 165 } 166 167 public static void diagnosticsDiff( 168 Map<Diagnostic, TextDiagnostic> diagnosticToExpectedDiagnostic, 169 List<DiagnosedRange> expected, 170 Collection<Diagnostic> actual, 171 DiagnosticDiffCallbacks callbacks 172 ) { 173 assertSameFile(actual); 174 175 Iterator<DiagnosedRange> expectedDiagnostics = expected.iterator(); 176 List<DiagnosticDescriptor> sortedDiagnosticDescriptors = getSortedDiagnosticDescriptors(actual); 177 Iterator<DiagnosticDescriptor> actualDiagnostics = sortedDiagnosticDescriptors.iterator(); 178 179 DiagnosedRange currentExpected = safeAdvance(expectedDiagnostics); 180 DiagnosticDescriptor currentActual = safeAdvance(actualDiagnostics); 181 while (currentExpected != null || currentActual != null) { 182 if (currentExpected != null) { 183 if (currentActual == null) { 184 missingDiagnostics(callbacks, currentExpected); 185 currentExpected = safeAdvance(expectedDiagnostics); 186 } 187 else { 188 int expectedStart = currentExpected.getStart(); 189 int actualStart = currentActual.getStart(); 190 int expectedEnd = currentExpected.getEnd(); 191 int actualEnd = currentActual.getEnd(); 192 if (expectedStart < actualStart) { 193 missingDiagnostics(callbacks, currentExpected); 194 currentExpected = safeAdvance(expectedDiagnostics); 195 } 196 else if (expectedStart > actualStart) { 197 unexpectedDiagnostics(currentActual.getDiagnostics(), callbacks); 198 currentActual = safeAdvance(actualDiagnostics); 199 } 200 else if (expectedEnd > actualEnd) { 201 assert expectedStart == actualStart; 202 missingDiagnostics(callbacks, currentExpected); 203 currentExpected = safeAdvance(expectedDiagnostics); 204 } 205 else if (expectedEnd < actualEnd) { 206 assert expectedStart == actualStart; 207 unexpectedDiagnostics(currentActual.getDiagnostics(), callbacks); 208 currentActual = safeAdvance(actualDiagnostics); 209 } 210 else { 211 compareDiagnostics(callbacks, currentExpected, currentActual, diagnosticToExpectedDiagnostic); 212 currentExpected = safeAdvance(expectedDiagnostics); 213 currentActual = safeAdvance(actualDiagnostics); 214 } 215 } 216 } 217 else { 218 //noinspection ConstantConditions 219 assert (currentActual != null); 220 221 unexpectedDiagnostics(currentActual.getDiagnostics(), callbacks); 222 currentActual = safeAdvance(actualDiagnostics); 223 } 224 } 225 } 226 227 private static void compareDiagnostics( 228 @NotNull DiagnosticDiffCallbacks callbacks, 229 @NotNull DiagnosedRange currentExpected, 230 @NotNull DiagnosticDescriptor currentActual, 231 @NotNull Map<Diagnostic, TextDiagnostic> diagnosticToInput 232 ) { 233 int expectedStart = currentExpected.getStart(); 234 int expectedEnd = currentExpected.getEnd(); 235 236 int actualStart = currentActual.getStart(); 237 int actualEnd = currentActual.getEnd(); 238 assert expectedStart == actualStart && expectedEnd == actualEnd; 239 240 Map<Diagnostic, TextDiagnostic> actualDiagnostics = currentActual.getTextDiagnosticsMap(); 241 List<TextDiagnostic> expectedDiagnostics = currentExpected.getDiagnostics(); 242 243 for (TextDiagnostic expectedDiagnostic : expectedDiagnostics) { 244 boolean diagnosticFound = false; 245 for (Diagnostic actualDiagnostic : actualDiagnostics.keySet()) { 246 TextDiagnostic actualTextDiagnostic = actualDiagnostics.get(actualDiagnostic); 247 if (expectedDiagnostic.getName().equals(actualTextDiagnostic.getName())) { 248 if (!compareTextDiagnostic(expectedDiagnostic, actualTextDiagnostic)) { 249 callbacks.wrongParametersDiagnostic(expectedDiagnostic, actualTextDiagnostic, expectedStart, expectedEnd); 250 } 251 252 actualDiagnostics.remove(actualDiagnostic); 253 diagnosticToInput.put(actualDiagnostic, expectedDiagnostic); 254 diagnosticFound = true; 255 break; 256 } 257 } 258 if (!diagnosticFound) callbacks.missingDiagnostic(expectedDiagnostic, expectedStart, expectedEnd); 259 } 260 261 for (TextDiagnostic unexpectedDiagnostic : actualDiagnostics.values()) { 262 callbacks.unexpectedDiagnostic(unexpectedDiagnostic, actualStart, actualEnd); 263 } 264 } 265 266 private static boolean compareTextDiagnostic(@NotNull TextDiagnostic expected, @NotNull TextDiagnostic actual) { 267 if (!expected.getName().equals(actual.getName())) return false; 268 269 if (expected.getParameters() == null) return true; 270 if (actual.getParameters() == null || expected.getParameters().size() != actual.getParameters().size()) return false; 271 272 for (int index = 0; index < expected.getParameters().size(); index++) { 273 String expectedParameter = expected.getParameters().get(index); 274 String actualParameter = actual.getParameters().get(index); 275 if (!expectedParameter.equals(IGNORE_DIAGNOSTIC_PARAMETER) && !expectedParameter.equals(actualParameter)) { 276 return false; 277 } 278 } 279 return true; 280 } 281 282 private static void assertSameFile(Collection<Diagnostic> actual) { 283 if (actual.isEmpty()) return; 284 PsiFile file = actual.iterator().next().getPsiElement().getContainingFile(); 285 for (Diagnostic diagnostic : actual) { 286 assert diagnostic.getPsiFile().equals(file) 287 : "All diagnostics should come from the same file: " + diagnostic.getPsiFile() + ", " + file; 288 } 289 } 290 291 private static void unexpectedDiagnostics(List<Diagnostic> actual, DiagnosticDiffCallbacks callbacks) { 292 for (Diagnostic diagnostic : actual) { 293 List<TextRange> textRanges = diagnostic.getTextRanges(); 294 for (TextRange textRange : textRanges) { 295 callbacks.unexpectedDiagnostic(TextDiagnostic.asTextDiagnostic(diagnostic), textRange.getStartOffset(), textRange.getEndOffset()); 296 } 297 } 298 } 299 300 private static void missingDiagnostics(DiagnosticDiffCallbacks callbacks, DiagnosedRange currentExpected) { 301 for (TextDiagnostic diagnostic : currentExpected.getDiagnostics()) { 302 callbacks.missingDiagnostic(diagnostic, currentExpected.getStart(), currentExpected.getEnd()); 303 } 304 } 305 306 private static <T> T safeAdvance(Iterator<T> iterator) { 307 return iterator.hasNext() ? iterator.next() : null; 308 } 309 310 public static String parseDiagnosedRanges(String text, List<DiagnosedRange> result) { 311 Matcher matcher = RANGE_START_OR_END_PATTERN.matcher(text); 312 313 Stack<DiagnosedRange> opened = new Stack<DiagnosedRange>(); 314 315 int offsetCompensation = 0; 316 317 while (matcher.find()) { 318 int effectiveOffset = matcher.start() - offsetCompensation; 319 String matchedText = matcher.group(); 320 if ("<!>".equals(matchedText)) { 321 opened.pop().setEnd(effectiveOffset); 322 } 323 else { 324 Matcher diagnosticTypeMatcher = INDIVIDUAL_DIAGNOSTIC_PATTERN.matcher(matchedText); 325 DiagnosedRange range = new DiagnosedRange(effectiveOffset); 326 while (diagnosticTypeMatcher.find()) { 327 range.addDiagnostic(diagnosticTypeMatcher.group()); 328 } 329 opened.push(range); 330 result.add(range); 331 } 332 offsetCompensation += matchedText.length(); 333 } 334 335 assert opened.isEmpty() : "Stack is not empty"; 336 337 matcher.reset(); 338 return matcher.replaceAll(""); 339 } 340 341 public static StringBuffer addDiagnosticMarkersToText(@NotNull PsiFile psiFile, @NotNull Collection<Diagnostic> diagnostics) { 342 return addDiagnosticMarkersToText(psiFile, diagnostics, Collections.<Diagnostic, TextDiagnostic>emptyMap(), new Function<PsiFile, String>() { 343 @Override 344 public String fun(PsiFile file) { 345 return file.getText(); 346 } 347 }); 348 } 349 350 public static StringBuffer addDiagnosticMarkersToText( 351 @NotNull final PsiFile psiFile, 352 @NotNull Collection<Diagnostic> diagnostics, 353 @NotNull Map<Diagnostic, TextDiagnostic> diagnosticToExpectedDiagnostic, 354 @NotNull Function<PsiFile, String> getFileText 355 ) { 356 String text = getFileText.fun(psiFile); 357 StringBuffer result = new StringBuffer(); 358 diagnostics = Collections2.filter(diagnostics, new Predicate<Diagnostic>() { 359 @Override 360 public boolean apply(Diagnostic diagnostic) { 361 return psiFile.equals(diagnostic.getPsiFile()); 362 } 363 }); 364 if (!diagnostics.isEmpty()) { 365 List<DiagnosticDescriptor> diagnosticDescriptors = getSortedDiagnosticDescriptors(diagnostics); 366 367 Stack<DiagnosticDescriptor> opened = new Stack<DiagnosticDescriptor>(); 368 ListIterator<DiagnosticDescriptor> iterator = diagnosticDescriptors.listIterator(); 369 DiagnosticDescriptor currentDescriptor = iterator.next(); 370 371 for (int i = 0; i < text.length(); i++) { 372 char c = text.charAt(i); 373 while (!opened.isEmpty() && i == opened.peek().end) { 374 closeDiagnosticString(result); 375 opened.pop(); 376 } 377 while (currentDescriptor != null && i == currentDescriptor.start) { 378 openDiagnosticsString(result, currentDescriptor, diagnosticToExpectedDiagnostic); 379 if (currentDescriptor.getEnd() == i) { 380 closeDiagnosticString(result); 381 } 382 else { 383 opened.push(currentDescriptor); 384 } 385 if (iterator.hasNext()) { 386 currentDescriptor = iterator.next(); 387 } 388 else { 389 currentDescriptor = null; 390 } 391 } 392 result.append(c); 393 } 394 395 if (currentDescriptor != null) { 396 assert currentDescriptor.start == text.length(); 397 assert currentDescriptor.end == text.length(); 398 openDiagnosticsString(result, currentDescriptor, diagnosticToExpectedDiagnostic); 399 opened.push(currentDescriptor); 400 } 401 402 while (!opened.isEmpty() && text.length() == opened.peek().end) { 403 closeDiagnosticString(result); 404 opened.pop(); 405 } 406 407 assert opened.isEmpty() : "Stack is not empty: " + opened; 408 409 } 410 else { 411 result.append(text); 412 } 413 return result; 414 } 415 416 private static void openDiagnosticsString( 417 StringBuffer result, 418 DiagnosticDescriptor currentDescriptor, 419 Map<Diagnostic, TextDiagnostic> diagnosticToExpectedDiagnostic 420 ) { 421 result.append("<!"); 422 for (Iterator<Diagnostic> iterator = currentDescriptor.diagnostics.iterator(); iterator.hasNext(); ) { 423 Diagnostic diagnostic = iterator.next(); 424 if (diagnosticToExpectedDiagnostic.containsKey(diagnostic)) { 425 TextDiagnostic expectedDiagnostic = diagnosticToExpectedDiagnostic.get(diagnostic); 426 TextDiagnostic actualTextDiagnostic = TextDiagnostic.asTextDiagnostic(diagnostic); 427 if (compareTextDiagnostic(expectedDiagnostic, actualTextDiagnostic)) { 428 result.append(expectedDiagnostic.asString()); 429 } else { 430 result.append(actualTextDiagnostic.asString()); 431 } 432 } else { 433 result.append(diagnostic.getFactory().getName()); 434 } 435 if (iterator.hasNext()) { 436 result.append(", "); 437 } 438 } 439 result.append("!>"); 440 } 441 442 private static void closeDiagnosticString(StringBuffer result) { 443 result.append("<!>"); 444 } 445 446 public static class AbstractDiagnosticForTests implements Diagnostic { 447 private final PsiElement element; 448 private final DiagnosticFactory<?> factory; 449 450 public AbstractDiagnosticForTests(@NotNull PsiElement element, @NotNull DiagnosticFactory<?> factory) { 451 this.element = element; 452 this.factory = factory; 453 } 454 455 @NotNull 456 @Override 457 public DiagnosticFactory<?> getFactory() { 458 return factory; 459 } 460 461 @NotNull 462 @Override 463 public Severity getSeverity() { 464 return Severity.ERROR; 465 } 466 467 @NotNull 468 @Override 469 public PsiElement getPsiElement() { 470 return element; 471 } 472 473 @NotNull 474 @Override 475 public List<TextRange> getTextRanges() { 476 return Collections.singletonList(element.getTextRange()); 477 } 478 479 @NotNull 480 @Override 481 public PsiFile getPsiFile() { 482 return element.getContainingFile(); 483 } 484 485 @Override 486 public boolean isValid() { 487 return true; 488 } 489 } 490 491 public static class SyntaxErrorDiagnosticFactory extends DiagnosticFactory<SyntaxErrorDiagnostic> { 492 public static final SyntaxErrorDiagnosticFactory INSTANCE = new SyntaxErrorDiagnosticFactory(); 493 494 private SyntaxErrorDiagnosticFactory() { 495 super(Severity.ERROR); 496 } 497 498 @NotNull 499 @Override 500 public String getName() { 501 return "SYNTAX"; 502 } 503 } 504 505 public static class SyntaxErrorDiagnostic extends AbstractDiagnosticForTests { 506 public SyntaxErrorDiagnostic(@NotNull PsiErrorElement errorElement) { 507 super(errorElement, SyntaxErrorDiagnosticFactory.INSTANCE); 508 } 509 } 510 511 public static class DebugInfoDiagnosticFactory extends DiagnosticFactory<DebugInfoDiagnostic> { 512 public static final DebugInfoDiagnosticFactory SMARTCAST = new DebugInfoDiagnosticFactory("SMARTCAST"); 513 public static final DebugInfoDiagnosticFactory ELEMENT_WITH_ERROR_TYPE = new DebugInfoDiagnosticFactory("ELEMENT_WITH_ERROR_TYPE"); 514 public static final DebugInfoDiagnosticFactory UNRESOLVED_WITH_TARGET = new DebugInfoDiagnosticFactory("UNRESOLVED_WITH_TARGET"); 515 public static final DebugInfoDiagnosticFactory MISSING_UNRESOLVED = new DebugInfoDiagnosticFactory("MISSING_UNRESOLVED"); 516 public static final DebugInfoDiagnosticFactory DYNAMIC = new DebugInfoDiagnosticFactory("DYNAMIC"); 517 518 private final String name; 519 private DebugInfoDiagnosticFactory(String name, Severity severity) { 520 super(severity); 521 this.name = name; 522 } 523 524 private DebugInfoDiagnosticFactory(String name) { 525 this(name, Severity.ERROR); 526 } 527 528 @NotNull 529 @Override 530 public String getName() { 531 return "DEBUG_INFO_" + name; 532 } 533 } 534 535 public static class DebugInfoDiagnostic extends AbstractDiagnosticForTests { 536 public DebugInfoDiagnostic(@NotNull JetElement element, @NotNull DebugInfoDiagnosticFactory factory) { 537 super(element, factory); 538 } 539 } 540 541 @NotNull 542 private static List<DiagnosticDescriptor> getSortedDiagnosticDescriptors(@NotNull Collection<Diagnostic> diagnostics) { 543 LinkedListMultimap<TextRange, Diagnostic> diagnosticsGroupedByRanges = LinkedListMultimap.create(); 544 for (Diagnostic diagnostic : diagnostics) { 545 if (!diagnostic.isValid()) continue; 546 for (TextRange textRange : diagnostic.getTextRanges()) { 547 diagnosticsGroupedByRanges.put(textRange, diagnostic); 548 } 549 } 550 List<DiagnosticDescriptor> diagnosticDescriptors = Lists.newArrayList(); 551 for (TextRange range : diagnosticsGroupedByRanges.keySet()) { 552 diagnosticDescriptors.add( 553 new DiagnosticDescriptor(range.getStartOffset(), range.getEndOffset(), diagnosticsGroupedByRanges.get(range))); 554 } 555 Collections.sort(diagnosticDescriptors, new Comparator<DiagnosticDescriptor>() { 556 @Override 557 public int compare(@NotNull DiagnosticDescriptor d1, @NotNull DiagnosticDescriptor d2) { 558 // Start early -- go first; start at the same offset, the one who end later is the outer, i.e. goes first 559 return (d1.start != d2.start) ? d1.start - d2.start : d2.end - d1.end; 560 } 561 }); 562 return diagnosticDescriptors; 563 } 564 565 private static class DiagnosticDescriptor { 566 private final int start; 567 private final int end; 568 private final List<Diagnostic> diagnostics; 569 570 DiagnosticDescriptor(int start, int end, List<Diagnostic> diagnostics) { 571 this.start = start; 572 this.end = end; 573 this.diagnostics = diagnostics; 574 } 575 576 public Map<Diagnostic, TextDiagnostic> getTextDiagnosticsMap() { 577 Map<Diagnostic, TextDiagnostic> diagnosticMap = new IdentityHashMap<Diagnostic, TextDiagnostic>(); 578 for (Diagnostic diagnostic : diagnostics) { 579 diagnosticMap.put(diagnostic, TextDiagnostic.asTextDiagnostic(diagnostic)); 580 } 581 return diagnosticMap; 582 } 583 584 public int getStart() { 585 return start; 586 } 587 588 public int getEnd() { 589 return end; 590 } 591 592 public List<Diagnostic> getDiagnostics() { 593 return diagnostics; 594 } 595 596 public TextRange getTextRange() { 597 return new TextRange(start, end); 598 } 599 } 600 601 public static class TextDiagnostic { 602 @NotNull 603 private static TextDiagnostic parseDiagnostic(String text) { 604 Matcher matcher = INDIVIDUAL_DIAGNOSTIC_PATTERN.matcher(text); 605 if (!matcher.find()) 606 throw new IllegalArgumentException("Could not parse diagnostic: " + text); 607 String name = matcher.group(1); 608 609 String parameters = matcher.group(2); 610 if (parameters == null) { 611 return new TextDiagnostic(name, null); 612 } 613 614 List<String> parsedParameters = new SmartList<String>(); 615 Matcher parametersMatcher = INDIVIDUAL_PARAMETER_PATTERN.matcher(parameters); 616 while (parametersMatcher.find()) 617 parsedParameters.add(unescape(parametersMatcher.group().trim())); 618 return new TextDiagnostic(name, parsedParameters); 619 } 620 621 private static @NotNull String escape(@NotNull String s) { 622 return s.replaceAll("([" + SHOULD_BE_ESCAPED + "])", "\\\\$1"); 623 } 624 625 private static @NotNull String unescape(@NotNull String s) { 626 return s.replaceAll("\\\\([" + SHOULD_BE_ESCAPED + "])", "$1"); 627 } 628 629 @NotNull 630 public static TextDiagnostic asTextDiagnostic(@NotNull Diagnostic diagnostic) { 631 DiagnosticRenderer renderer = DefaultErrorMessages.getRendererForDiagnostic(diagnostic); 632 String diagnosticName = diagnostic.getFactory().getName(); 633 if (renderer instanceof AbstractDiagnosticWithParametersRenderer) { 634 //noinspection unchecked 635 Object[] renderParameters = ((AbstractDiagnosticWithParametersRenderer) renderer).renderParameters(diagnostic); 636 List<String> parameters = ContainerUtil.map(renderParameters, new Function<Object, String>() { 637 @Override 638 public String fun(Object o) { 639 return o != null ? o.toString() : "null"; 640 } 641 }); 642 return new TextDiagnostic(diagnosticName, parameters); 643 } 644 return new TextDiagnostic(diagnosticName, null); 645 } 646 647 @NotNull 648 private final String name; 649 @Nullable 650 private final List<String> parameters; 651 652 public TextDiagnostic(@NotNull String name, @Nullable List<String> parameters) { 653 this.name = name; 654 this.parameters = parameters; 655 } 656 657 @NotNull 658 public String getName() { 659 return name; 660 } 661 662 @Nullable 663 public List<String> getParameters() { 664 return parameters; 665 } 666 667 @Override 668 public boolean equals(Object o) { 669 if (this == o) return true; 670 if (o == null || getClass() != o.getClass()) return false; 671 672 TextDiagnostic that = (TextDiagnostic) o; 673 674 if (!name.equals(that.name)) return false; 675 if (parameters != null ? !parameters.equals(that.parameters) : that.parameters != null) return false; 676 677 return true; 678 } 679 680 @Override 681 public int hashCode() { 682 int result = name.hashCode(); 683 result = 31 * result + (parameters != null ? parameters.hashCode() : 0); 684 return result; 685 } 686 687 @NotNull 688 public String asString() { 689 if (parameters == null) 690 return name; 691 return name + '(' + StringUtil.join(parameters, new Function<String, String>() { 692 @Override 693 public String fun(String s) { 694 return escape(s); 695 } 696 }, "; ") + ')'; 697 } 698 } 699 700 public static class DiagnosedRange { 701 private final int start; 702 private int end; 703 private final List<TextDiagnostic> diagnostics = ContainerUtil.newSmartList(); 704 private PsiFile file; 705 706 protected DiagnosedRange(int start) { 707 this.start = start; 708 } 709 710 public int getStart() { 711 return start; 712 } 713 714 public int getEnd() { 715 return end; 716 } 717 718 public List<TextDiagnostic> getDiagnostics() { 719 return diagnostics; 720 } 721 722 public void setEnd(int end) { 723 this.end = end; 724 } 725 726 public void addDiagnostic(String diagnostic) { 727 diagnostics.add(TextDiagnostic.parseDiagnostic(diagnostic)); 728 } 729 730 public void setFile(@NotNull PsiFile file) { 731 this.file = file; 732 } 733 734 @NotNull 735 public PsiFile getFile() { 736 return file; 737 } 738 } 739 }