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