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