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