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