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