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    }