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.KtElement;
043 import org.jetbrains.kotlin.psi.KtExpression;
044 import org.jetbrains.kotlin.psi.KtReferenceExpression;
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 KtReferenceExpression expression) {
123 newDiagnostic(expression, DebugInfoDiagnosticFactory.ELEMENT_WITH_ERROR_TYPE);
124 }
125
126 @Override
127 public void reportMissingUnresolved(@NotNull KtReferenceExpression expression) {
128 newDiagnostic(expression, DebugInfoDiagnosticFactory.MISSING_UNRESOLVED);
129 }
130
131 @Override
132 public void reportUnresolvedWithTarget(@NotNull KtReferenceExpression expression, @NotNull String target) {
133 newDiagnostic(expression, DebugInfoDiagnosticFactory.UNRESOLVED_WITH_TARGET);
134 }
135
136 @Override
137 public void reportDynamicCall(@NotNull KtElement 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(KtElement 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 (KtExpression 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 KtElement 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 }