001    /*
002     * Copyright 2010-2013 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.jet.checkers;
018    
019    import com.google.common.base.Predicate;
020    import com.google.common.collect.*;
021    import com.intellij.openapi.util.TextRange;
022    import com.intellij.psi.PsiElement;
023    import com.intellij.psi.PsiErrorElement;
024    import com.intellij.psi.PsiFile;
025    import com.intellij.psi.util.PsiTreeUtil;
026    import com.intellij.util.containers.Stack;
027    import org.jetbrains.annotations.NotNull;
028    import org.jetbrains.jet.lang.diagnostics.Diagnostic;
029    import org.jetbrains.jet.lang.diagnostics.DiagnosticFactory;
030    import org.jetbrains.jet.lang.diagnostics.Severity;
031    import org.jetbrains.jet.lang.psi.JetReferenceExpression;
032    import org.jetbrains.jet.lang.resolve.AnalyzingUtils;
033    import org.jetbrains.jet.lang.resolve.BindingContext;
034    
035    import java.util.*;
036    import java.util.regex.Matcher;
037    import java.util.regex.Pattern;
038    
039    public class CheckerTestUtil {
040        public static final Comparator<Diagnostic> DIAGNOSTIC_COMPARATOR = new Comparator<Diagnostic>() {
041            @Override
042            public int compare(@NotNull Diagnostic o1, @NotNull Diagnostic o2) {
043                List<TextRange> ranges1 = o1.getTextRanges();
044                List<TextRange> ranges2 = o2.getTextRanges();
045                if (ranges1.size() != ranges2.size()) return ranges1.size() - ranges2.size();
046                for (int i = 0; i < ranges1.size(); i++) {
047                    TextRange range1 = ranges1.get(i);
048                    TextRange range2 = ranges2.get(i);
049                    int startOffset1 = range1.getStartOffset();
050                    int startOffset2 = range2.getStartOffset();
051                    if (startOffset1 != startOffset2) {
052                        // Start early -- go first
053                        return startOffset1 - range2.getStartOffset();
054                    }
055                    int endOffset1 = range1.getEndOffset();
056                    int endOffset2 = range2.getEndOffset();
057                    if (endOffset1 != endOffset2) {
058                        // start at the same offset, the one who end later is the outer, i.e. goes first
059                        return endOffset2 - endOffset1;
060                    }
061                }
062                return 0;
063            }
064        };
065        private static final Pattern RANGE_START_OR_END_PATTERN = Pattern.compile("(<!\\w+(,\\s*\\w+)*!>)|(<!>)");
066        private static final Pattern INDIVIDUAL_DIAGNOSTIC_PATTERN = Pattern.compile("\\w+");
067    
068        public static List<Diagnostic> getDiagnosticsIncludingSyntaxErrors(BindingContext bindingContext, final PsiElement root) {
069            ArrayList<Diagnostic> diagnostics = new ArrayList<Diagnostic>();
070            diagnostics.addAll(Collections2.filter(bindingContext.getDiagnostics().all(),
071                                                   new Predicate<Diagnostic>() {
072                                                       @Override
073                                                       public boolean apply(Diagnostic diagnostic) {
074                                                           return  PsiTreeUtil.isAncestor(root, diagnostic.getPsiElement(), false);
075                                                       }
076                                                   }));
077            for (PsiErrorElement errorElement : AnalyzingUtils.getSyntaxErrorRanges(root)) {
078                diagnostics.add(new SyntaxErrorDiagnostic(errorElement));
079            }
080            List<Diagnostic> debugAnnotations = getDebugInfoDiagnostics(root, bindingContext);
081            diagnostics.addAll(debugAnnotations);
082            return diagnostics;
083        }
084    
085        public static List<Diagnostic> getDebugInfoDiagnostics(@NotNull PsiElement root, @NotNull BindingContext bindingContext) {
086            final List<Diagnostic> debugAnnotations = Lists.newArrayList();
087            DebugInfoUtil.markDebugAnnotations(root, bindingContext, new DebugInfoUtil.DebugInfoReporter() {
088                @Override
089                public void reportElementWithErrorType(@NotNull JetReferenceExpression expression) {
090                    newDiagnostic(expression, DebugInfoDiagnosticFactory.ELEMENT_WITH_ERROR_TYPE);
091                }
092    
093                @Override
094                public void reportMissingUnresolved(@NotNull JetReferenceExpression expression) {
095                    newDiagnostic(expression, DebugInfoDiagnosticFactory.MISSING_UNRESOLVED);
096                }
097    
098                @Override
099                public void reportUnresolvedWithTarget(@NotNull JetReferenceExpression expression, @NotNull String target) {
100                    newDiagnostic(expression, DebugInfoDiagnosticFactory.UNRESOLVED_WITH_TARGET);
101                }
102    
103                private void newDiagnostic(JetReferenceExpression expression, DebugInfoDiagnosticFactory factory) {
104                    debugAnnotations.add(new DebugInfoDiagnostic(expression, factory));
105                }
106            });
107            return debugAnnotations;
108        }
109    
110        public interface DiagnosticDiffCallbacks {
111            void missingDiagnostic(String type, int expectedStart, int expectedEnd);
112            void unexpectedDiagnostic(String type, int actualStart, int actualEnd);
113        }
114    
115        public static void diagnosticsDiff(
116                List<DiagnosedRange> expected,
117                Collection<Diagnostic> actual,
118                DiagnosticDiffCallbacks callbacks
119        ) {
120            assertSameFile(actual);
121    
122            Iterator<DiagnosedRange> expectedDiagnostics = expected.iterator();
123            List<DiagnosticDescriptor> sortedDiagnosticDescriptors = getSortedDiagnosticDescriptors(actual);
124            Iterator<DiagnosticDescriptor> actualDiagnostics = sortedDiagnosticDescriptors.iterator();
125    
126            DiagnosedRange currentExpected = safeAdvance(expectedDiagnostics);
127            DiagnosticDescriptor currentActual = safeAdvance(actualDiagnostics);
128            while (currentExpected != null || currentActual != null) {
129                if (currentExpected != null) {
130                    if (currentActual == null) {
131                        missingDiagnostics(callbacks, currentExpected);
132                        currentExpected = safeAdvance(expectedDiagnostics);
133                    }
134                    else {
135                        int expectedStart = currentExpected.getStart();
136                        int actualStart = currentActual.getStart();
137                        int expectedEnd = currentExpected.getEnd();
138                        int actualEnd = currentActual.getEnd();
139                        if (expectedStart < actualStart) {
140                            missingDiagnostics(callbacks, currentExpected);
141                            currentExpected = safeAdvance(expectedDiagnostics);
142                        }
143                        else if (expectedStart > actualStart) {
144                            unexpectedDiagnostics(currentActual.getDiagnostics(), callbacks);
145                            currentActual = safeAdvance(actualDiagnostics);
146                        }
147                        else if (expectedEnd > actualEnd) {
148                            assert expectedStart == actualStart;
149                            missingDiagnostics(callbacks, currentExpected);
150                            currentExpected = safeAdvance(expectedDiagnostics);
151                        }
152                        else if (expectedEnd < actualEnd) {
153                            assert expectedStart == actualStart;
154                            unexpectedDiagnostics(currentActual.getDiagnostics(), callbacks);
155                            currentActual = safeAdvance(actualDiagnostics);
156                        }
157                        else {
158                            assert expectedStart == actualStart && expectedEnd == actualEnd;
159                            Multiset<String> actualDiagnosticTypes = currentActual.getDiagnosticTypeStrings();
160                            Multiset<String> expectedDiagnosticTypes = currentExpected.getDiagnostics();
161                            if (!actualDiagnosticTypes.equals(expectedDiagnosticTypes)) {
162                                Multiset<String> notInActualTypes = HashMultiset.create(expectedDiagnosticTypes);
163                                Multisets.removeOccurrences(notInActualTypes, actualDiagnosticTypes);
164    
165                                Multiset<String> notInExpectedTypes = HashMultiset.create(actualDiagnosticTypes);
166                                Multisets.removeOccurrences(notInExpectedTypes, expectedDiagnosticTypes);
167    
168                                for (String type : notInActualTypes) {
169                                    callbacks.missingDiagnostic(type, expectedStart, expectedEnd);
170                                }
171                                for (String type : notInExpectedTypes) {
172                                    callbacks.unexpectedDiagnostic(type, actualStart, actualEnd);
173                                }
174                            }
175                            currentExpected = safeAdvance(expectedDiagnostics);
176                            currentActual = safeAdvance(actualDiagnostics);
177                        }
178                    }
179                }
180                else {
181                    //noinspection ConstantConditions
182                    assert (currentActual != null);
183    
184                    unexpectedDiagnostics(currentActual.getDiagnostics(), callbacks);
185                    currentActual = safeAdvance(actualDiagnostics);
186                }
187            }
188        }
189    
190        private static void assertSameFile(Collection<Diagnostic> actual) {
191            if (actual.isEmpty()) return;
192            PsiFile file = actual.iterator().next().getPsiElement().getContainingFile();
193            for (Diagnostic diagnostic : actual) {
194                assert diagnostic.getPsiFile().equals(file)
195                        : "All diagnostics should come from the same file: " + diagnostic.getPsiFile() + ", " + file;
196            }
197        }
198    
199        private static void unexpectedDiagnostics(List<Diagnostic> actual, DiagnosticDiffCallbacks callbacks) {
200            for (Diagnostic diagnostic : actual) {
201                List<TextRange> textRanges = diagnostic.getTextRanges();
202                for (TextRange textRange : textRanges) {
203                    callbacks.unexpectedDiagnostic(diagnostic.getFactory().getName(), textRange.getStartOffset(), textRange.getEndOffset());
204                }
205            }
206        }
207    
208        private static void missingDiagnostics(DiagnosticDiffCallbacks callbacks, DiagnosedRange currentExpected) {
209            for (String type : currentExpected.getDiagnostics()) {
210                callbacks.missingDiagnostic(type, currentExpected.getStart(), currentExpected.getEnd());
211            }
212        }
213    
214        private static <T> T safeAdvance(Iterator<T> iterator) {
215            return iterator.hasNext() ? iterator.next() : null;
216        }
217    
218        public static String parseDiagnosedRanges(String text, List<DiagnosedRange> result) {
219            Matcher matcher = RANGE_START_OR_END_PATTERN.matcher(text);
220    
221            Stack<DiagnosedRange> opened = new Stack<DiagnosedRange>();
222    
223            int offsetCompensation = 0;
224    
225            while (matcher.find()) {
226                int effectiveOffset = matcher.start() - offsetCompensation;
227                String matchedText = matcher.group();
228                if ("<!>".equals(matchedText)) {
229                    opened.pop().setEnd(effectiveOffset);
230                }
231                else {
232                    Matcher diagnosticTypeMatcher = INDIVIDUAL_DIAGNOSTIC_PATTERN.matcher(matchedText);
233                    DiagnosedRange range = new DiagnosedRange(effectiveOffset);
234                    while (diagnosticTypeMatcher.find()) {
235                        range.addDiagnostic(diagnosticTypeMatcher.group());
236                    }
237                    opened.push(range);
238                    result.add(range);
239                }
240                offsetCompensation += matchedText.length();
241            }
242    
243            assert opened.isEmpty() : "Stack is not empty";
244    
245            matcher.reset();
246            return matcher.replaceAll("");
247        }
248        
249        public static StringBuffer addDiagnosticMarkersToText(@NotNull final PsiFile psiFile, Collection<Diagnostic> diagnostics) {
250            StringBuffer result = new StringBuffer();
251            String text = psiFile.getText();
252            diagnostics = Collections2.filter(diagnostics, new Predicate<Diagnostic>() {
253                @Override
254                public boolean apply(Diagnostic diagnostic) {
255                    return psiFile.equals(diagnostic.getPsiFile());
256                }
257            });
258            if (!diagnostics.isEmpty()) {
259                List<DiagnosticDescriptor> diagnosticDescriptors = getSortedDiagnosticDescriptors(diagnostics);
260    
261                Stack<DiagnosticDescriptor> opened = new Stack<DiagnosticDescriptor>();
262                ListIterator<DiagnosticDescriptor> iterator = diagnosticDescriptors.listIterator();
263                DiagnosticDescriptor currentDescriptor = iterator.next();
264    
265                for (int i = 0; i < text.length(); i++) {
266                    char c = text.charAt(i);
267                    while (!opened.isEmpty() && i == opened.peek().end) {
268                        closeDiagnosticString(result);
269                        opened.pop();
270                    }
271                    while (currentDescriptor != null && i == currentDescriptor.start) {
272                        openDiagnosticsString(result, currentDescriptor);
273                        if (currentDescriptor.getEnd() == i) {
274                            closeDiagnosticString(result);
275                        }
276                        else {
277                            opened.push(currentDescriptor);
278                        }
279                        if (iterator.hasNext()) {
280                            currentDescriptor = iterator.next();
281                        }
282                        else {
283                            currentDescriptor = null;
284                        }
285                    }
286                    result.append(c);
287                }
288                
289                if (currentDescriptor != null) {
290                    assert currentDescriptor.start == text.length();
291                    assert currentDescriptor.end == text.length();
292                    openDiagnosticsString(result, currentDescriptor);
293                    opened.push(currentDescriptor);
294                }
295                
296                while (!opened.isEmpty() && text.length() == opened.peek().end) {
297                    closeDiagnosticString(result);
298                    opened.pop();
299                }
300    
301                assert opened.isEmpty() : "Stack is not empty: " + opened;
302    
303            }
304            else {
305                result.append(text);
306            }
307            return result;
308        }
309    
310        private static void openDiagnosticsString(StringBuffer result, DiagnosticDescriptor currentDescriptor) {
311            result.append("<!");
312            for (Iterator<Diagnostic> iterator = currentDescriptor.diagnostics.iterator(); iterator.hasNext(); ) {
313                Diagnostic diagnostic = iterator.next();
314                result.append(diagnostic.getFactory().getName());
315                if (iterator.hasNext()) {
316                    result.append(", ");
317                }
318            }
319            result.append("!>");
320        }
321    
322        private static void closeDiagnosticString(StringBuffer result) {
323            result.append("<!>");
324        }
325    
326        public static class AbstractDiagnosticForTests implements Diagnostic {
327            private final PsiElement element;
328            private final DiagnosticFactory factory;
329    
330            public AbstractDiagnosticForTests(@NotNull PsiElement element, @NotNull DiagnosticFactory factory) {
331                this.element = element;
332                this.factory = factory;
333            }
334    
335            @NotNull
336            @Override
337            public DiagnosticFactory getFactory() {
338                return factory;
339            }
340    
341            @NotNull
342            @Override
343            public Severity getSeverity() {
344                return Severity.ERROR;
345            }
346    
347            @NotNull
348            @Override
349            public PsiElement getPsiElement() {
350                return element;
351            }
352    
353            @NotNull
354            @Override
355            public List<TextRange> getTextRanges() {
356                return Collections.singletonList(element.getTextRange());
357            }
358    
359            @NotNull
360            @Override
361            public PsiFile getPsiFile() {
362                return element.getContainingFile();
363            }
364    
365            @Override
366            public boolean isValid() {
367                return true;
368            }
369        }
370    
371        public static class SyntaxErrorDiagnosticFactory extends DiagnosticFactory {
372            public static final SyntaxErrorDiagnosticFactory INSTANCE = new SyntaxErrorDiagnosticFactory();
373    
374            private SyntaxErrorDiagnosticFactory() {
375                super(Severity.ERROR);
376            }
377    
378            @NotNull
379            @Override
380            public String getName() {
381                return "SYNTAX";
382            }
383        }
384    
385        public static class SyntaxErrorDiagnostic extends AbstractDiagnosticForTests {
386            public SyntaxErrorDiagnostic(@NotNull PsiErrorElement errorElement) {
387                super(errorElement, SyntaxErrorDiagnosticFactory.INSTANCE);
388            }
389        }
390    
391        public static class DebugInfoDiagnosticFactory extends DiagnosticFactory {
392            public static final DebugInfoDiagnosticFactory ELEMENT_WITH_ERROR_TYPE = new DebugInfoDiagnosticFactory("ELEMENT_WITH_ERROR_TYPE");
393            public static final DebugInfoDiagnosticFactory UNRESOLVED_WITH_TARGET = new DebugInfoDiagnosticFactory("UNRESOLVED_WITH_TARGET");
394            public static final DebugInfoDiagnosticFactory MISSING_UNRESOLVED = new DebugInfoDiagnosticFactory("MISSING_UNRESOLVED");
395    
396            private final String name;
397            private DebugInfoDiagnosticFactory(String name) {
398                super(Severity.ERROR);
399                this.name = name;
400            }
401    
402            @NotNull
403            @Override
404            public String getName() {
405                return "DEBUG_INFO_" + name;
406            }
407        }
408    
409        public static class DebugInfoDiagnostic extends AbstractDiagnosticForTests {
410            public DebugInfoDiagnostic(@NotNull JetReferenceExpression reference, @NotNull DebugInfoDiagnosticFactory factory) {
411                super(reference, factory);
412            }
413        }
414    
415        private static List<DiagnosticDescriptor> getSortedDiagnosticDescriptors(Collection<Diagnostic> diagnostics) {
416            List<Diagnostic> list = Lists.newArrayList(diagnostics);
417            Collections.sort(list, DIAGNOSTIC_COMPARATOR);
418    
419            List<DiagnosticDescriptor> diagnosticDescriptors = Lists.newArrayList();
420            DiagnosticDescriptor currentDiagnosticDescriptor = null;
421            for (Diagnostic diagnostic : list) {
422                List<TextRange> textRanges = diagnostic.getTextRanges();
423                if (!diagnostic.isValid()) continue;
424    
425                TextRange textRange = textRanges.get(0);
426                if (currentDiagnosticDescriptor != null && currentDiagnosticDescriptor.equalRange(textRange)) {
427                    currentDiagnosticDescriptor.diagnostics.add(diagnostic);
428                }
429                else {
430                    currentDiagnosticDescriptor = new DiagnosticDescriptor(textRange.getStartOffset(), textRange.getEndOffset(), diagnostic);
431                    diagnosticDescriptors.add(currentDiagnosticDescriptor);
432                }
433            }
434            return diagnosticDescriptors;
435        }
436    
437        private static class DiagnosticDescriptor {
438            private final int start;
439            private final int end;
440            private final List<Diagnostic> diagnostics = Lists.newArrayList();
441    
442            DiagnosticDescriptor(int start, int end, Diagnostic diagnostic) {
443                this.start = start;
444                this.end = end;
445                this.diagnostics.add(diagnostic);
446            }
447    
448            public boolean equalRange(TextRange textRange) {
449                return start == textRange.getStartOffset() && end == textRange.getEndOffset();
450            }
451    
452            public Multiset<String> getDiagnosticTypeStrings() {
453                Multiset<String> actualDiagnosticTypes = HashMultiset.create();
454                for (Diagnostic diagnostic : diagnostics) {
455                    actualDiagnosticTypes.add(diagnostic.getFactory().getName());
456                }
457                return actualDiagnosticTypes;
458            }
459    
460            public int getStart() {
461                return start;
462            }
463    
464            public int getEnd() {
465                return end;
466            }
467    
468            public List<Diagnostic> getDiagnostics() {
469                return diagnostics;
470            }
471    
472            public TextRange getTextRange() {
473                return new TextRange(start, end);
474            }
475        }
476    
477        public static class DiagnosedRange {
478            private final int start;
479            private int end;
480            private final Multiset<String> diagnostics = HashMultiset.create();
481            private PsiFile file;
482    
483            private DiagnosedRange(int start) {
484                this.start = start;
485            }
486    
487            public int getStart() {
488                return start;
489            }
490    
491            public int getEnd() {
492                return end;
493            }
494    
495            public Multiset<String> getDiagnostics() {
496                return diagnostics;
497            }
498    
499            public void setEnd(int end) {
500                this.end = end;
501            }
502            
503            public void addDiagnostic(String diagnostic) {
504                diagnostics.add(diagnostic);
505            }
506    
507            public void setFile(@NotNull PsiFile file) {
508                this.file = file;
509            }
510    
511            @NotNull
512            public PsiFile getFile() {
513                return file;
514            }
515        }
516    }