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.AbstractDiagnosticFactory;
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(),
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 AbstractDiagnosticFactory factory;
333    
334            public AbstractDiagnosticForTests(@NotNull PsiElement element, @NotNull AbstractDiagnosticFactory factory) {
335                this.element = element;
336                this.factory = factory;
337            }
338    
339            @NotNull
340            @Override
341            public AbstractDiagnosticFactory getFactory() {
342                return factory;
343            }
344    
345            @NotNull
346            @Override
347            public Severity getSeverity() {
348                throw new IllegalStateException();
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        private static class SyntaxErrorDiagnosticFactory extends AbstractDiagnosticFactory {
376            public static final SyntaxErrorDiagnosticFactory INSTANCE = new SyntaxErrorDiagnosticFactory();
377    
378            private SyntaxErrorDiagnosticFactory() {}
379    
380            @NotNull
381            @Override
382            public String getName() {
383                return "SYNTAX";
384            }
385        }
386    
387        public static class SyntaxErrorDiagnostic extends AbstractDiagnosticForTests {
388            public SyntaxErrorDiagnostic(@NotNull PsiErrorElement errorElement) {
389                super(errorElement, SyntaxErrorDiagnosticFactory.INSTANCE);
390            }
391        }
392    
393        public static class DebugInfoDiagnosticFactory extends AbstractDiagnosticFactory {
394            public static final DebugInfoDiagnosticFactory ELEMENT_WITH_ERROR_TYPE = new DebugInfoDiagnosticFactory("ELEMENT_WITH_ERROR_TYPE");
395            public static final DebugInfoDiagnosticFactory UNRESOLVED_WITH_TARGET = new DebugInfoDiagnosticFactory("UNRESOLVED_WITH_TARGET");
396            public static final DebugInfoDiagnosticFactory MISSING_UNRESOLVED = new DebugInfoDiagnosticFactory("MISSING_UNRESOLVED");
397    
398            private final String name;
399            private DebugInfoDiagnosticFactory(String name) {
400                this.name = name;
401            }
402    
403            @NotNull
404            @Override
405            public String getName() {
406                return "DEBUG_INFO_" + name;
407            }
408        }
409    
410        public static class DebugInfoDiagnostic extends AbstractDiagnosticForTests {
411            public DebugInfoDiagnostic(@NotNull JetReferenceExpression reference, @NotNull DebugInfoDiagnosticFactory factory) {
412                super(reference, factory);
413            }
414        }
415    
416        private static List<DiagnosticDescriptor> getSortedDiagnosticDescriptors(Collection<Diagnostic> diagnostics) {
417            List<Diagnostic> list = Lists.newArrayList(diagnostics);
418            Collections.sort(list, DIAGNOSTIC_COMPARATOR);
419    
420            List<DiagnosticDescriptor> diagnosticDescriptors = Lists.newArrayList();
421            DiagnosticDescriptor currentDiagnosticDescriptor = null;
422            for (Diagnostic diagnostic : list) {
423                List<TextRange> textRanges = diagnostic.getTextRanges();
424                if (!diagnostic.isValid()) continue;
425    
426                TextRange textRange = textRanges.get(0);
427                if (currentDiagnosticDescriptor != null && currentDiagnosticDescriptor.equalRange(textRange)) {
428                    currentDiagnosticDescriptor.diagnostics.add(diagnostic);
429                }
430                else {
431                    currentDiagnosticDescriptor = new DiagnosticDescriptor(textRange.getStartOffset(), textRange.getEndOffset(), diagnostic);
432                    diagnosticDescriptors.add(currentDiagnosticDescriptor);
433                }
434            }
435            return diagnosticDescriptors;
436        }
437    
438        private static class DiagnosticDescriptor {
439            private final int start;
440            private final int end;
441            private final List<Diagnostic> diagnostics = Lists.newArrayList();
442    
443            DiagnosticDescriptor(int start, int end, Diagnostic diagnostic) {
444                this.start = start;
445                this.end = end;
446                this.diagnostics.add(diagnostic);
447            }
448    
449            public boolean equalRange(TextRange textRange) {
450                return start == textRange.getStartOffset() && end == textRange.getEndOffset();
451            }
452    
453            public Multiset<String> getDiagnosticTypeStrings() {
454                Multiset<String> actualDiagnosticTypes = HashMultiset.create();
455                for (Diagnostic diagnostic : diagnostics) {
456                    actualDiagnosticTypes.add(diagnostic.getFactory().getName());
457                }
458                return actualDiagnosticTypes;
459            }
460    
461            public int getStart() {
462                return start;
463            }
464    
465            public int getEnd() {
466                return end;
467            }
468    
469            public List<Diagnostic> getDiagnostics() {
470                return diagnostics;
471            }
472    
473            public TextRange getTextRange() {
474                return new TextRange(start, end);
475            }
476        }
477    
478        public static class DiagnosedRange {
479            private final int start;
480            private int end;
481            private final Multiset<String> diagnostics = HashMultiset.create();
482            private PsiFile file;
483    
484            private DiagnosedRange(int start) {
485                this.start = start;
486            }
487    
488            public int getStart() {
489                return start;
490            }
491    
492            public int getEnd() {
493                return end;
494            }
495    
496            public Multiset<String> getDiagnostics() {
497                return diagnostics;
498            }
499    
500            public void setEnd(int end) {
501                this.end = end;
502            }
503            
504            public void addDiagnostic(String diagnostic) {
505                diagnostics.add(diagnostic);
506            }
507    
508            public void setFile(@NotNull PsiFile file) {
509                this.file = file;
510            }
511    
512            @NotNull
513            public PsiFile getFile() {
514                return file;
515            }
516        }
517    }