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