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
017package org.jetbrains.jet.checkers;
018
019import com.google.common.base.Predicate;
020import com.google.common.collect.Collections2;
021import com.google.common.collect.HashMultiset;
022import com.google.common.collect.Lists;
023import com.google.common.collect.Multiset;
024import com.intellij.openapi.util.TextRange;
025import com.intellij.psi.PsiElement;
026import com.intellij.psi.PsiErrorElement;
027import com.intellij.psi.PsiFile;
028import com.intellij.psi.util.PsiTreeUtil;
029import com.intellij.util.containers.Stack;
030import org.jetbrains.annotations.NotNull;
031import org.jetbrains.jet.lang.diagnostics.AbstractDiagnosticFactory;
032import org.jetbrains.jet.lang.diagnostics.Diagnostic;
033import org.jetbrains.jet.lang.diagnostics.Severity;
034import org.jetbrains.jet.lang.psi.JetReferenceExpression;
035import org.jetbrains.jet.lang.resolve.AnalyzingUtils;
036import org.jetbrains.jet.lang.resolve.BindingContext;
037
038import java.util.*;
039import java.util.regex.Matcher;
040import java.util.regex.Pattern;
041
042public 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}