001    /*
002     * Copyright 2010-2015 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.kotlin.resolve.diagnostics;
018    
019    import com.google.common.collect.ImmutableSet;
020    import com.intellij.openapi.diagnostic.Logger;
021    import com.intellij.openapi.extensions.ExtensionPointName;
022    import com.intellij.openapi.util.Condition;
023    import com.intellij.openapi.util.ModificationTracker;
024    import com.intellij.psi.PsiElement;
025    import com.intellij.psi.PsiFile;
026    import com.intellij.util.containers.ConcurrentWeakValueHashMap;
027    import com.intellij.util.containers.FilteringIterator;
028    import kotlin.CollectionsKt;
029    import kotlin.jvm.functions.Function1;
030    import org.jetbrains.annotations.NotNull;
031    import org.jetbrains.annotations.TestOnly;
032    import org.jetbrains.kotlin.builtins.KotlinBuiltIns;
033    import org.jetbrains.kotlin.descriptors.DeclarationDescriptor;
034    import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor;
035    import org.jetbrains.kotlin.diagnostics.Diagnostic;
036    import org.jetbrains.kotlin.diagnostics.Severity;
037    import org.jetbrains.kotlin.psi.*;
038    import org.jetbrains.kotlin.resolve.BindingContext;
039    import org.jetbrains.kotlin.resolve.constants.ArrayValue;
040    import org.jetbrains.kotlin.resolve.constants.ConstantValue;
041    import org.jetbrains.kotlin.resolve.constants.StringValue;
042    import org.jetbrains.kotlin.util.ExtensionProvider;
043    
044    import java.util.*;
045    
046    public class DiagnosticsWithSuppression implements Diagnostics {
047    
048        public interface SuppressStringProvider {
049            ExtensionPointName<SuppressStringProvider> EP_NAME = ExtensionPointName.create("org.jetbrains.kotlin.suppressStringProvider");
050    
051            @NotNull
052            List<String> get(@NotNull AnnotationDescriptor annotationDescriptor);
053        }
054    
055        public interface DiagnosticSuppressor {
056            ExtensionPointName<DiagnosticSuppressor> EP_NAME = ExtensionPointName.create("org.jetbrains.kotlin.diagnosticSuppressor");
057    
058            boolean isSuppressed(@NotNull Diagnostic diagnostic);
059        }
060    
061        private static final Logger LOG = Logger.getInstance(DiagnosticsWithSuppression.class);
062    
063        private static final ExtensionProvider<SuppressStringProvider> ADDITIONAL_SUPPRESS_STRING_PROVIDERS
064                = ExtensionProvider.create(SuppressStringProvider.EP_NAME);
065        private static final ExtensionProvider<DiagnosticSuppressor> DIAGNOSTIC_SUPPRESSORS
066                = ExtensionProvider.create(DiagnosticSuppressor.EP_NAME);
067    
068        private final BindingContext context;
069        private final Collection<Diagnostic> diagnostics;
070    
071        // The cache is weak: we're OK with losing it
072        private final Map<KtAnnotated, Suppressor> suppressors = new ConcurrentWeakValueHashMap<KtAnnotated, Suppressor>();
073    
074        private final Function1<Diagnostic, Boolean> filter = new Function1<Diagnostic, Boolean>() {
075            @Override
076            public Boolean invoke(Diagnostic diagnostic) {
077                return !isSuppressed(diagnostic);
078            }
079        };
080    
081        private final DiagnosticsElementsCache elementsCache = new DiagnosticsElementsCache(this, filter);
082    
083        public DiagnosticsWithSuppression(@NotNull BindingContext context, @NotNull Collection<Diagnostic> diagnostics) {
084            this.context = context;
085            this.diagnostics = diagnostics;
086        }
087    
088        @NotNull
089        @Override
090        public Diagnostics noSuppression() {
091            return new SimpleDiagnostics(diagnostics);
092        }
093    
094        @NotNull
095        @Override
096        public Iterator<Diagnostic> iterator() {
097            return new FilteringIterator<Diagnostic, Diagnostic>(diagnostics.iterator(), new Condition<Diagnostic>() {
098                @Override
099                public boolean value(Diagnostic diagnostic) {
100                    return filter.invoke(diagnostic);
101                }
102            });
103        }
104    
105        @NotNull
106        @Override
107        public Collection<Diagnostic> all() {
108            return CollectionsKt.filter(diagnostics, filter);
109        }
110    
111        @NotNull
112        @Override
113        public Collection<Diagnostic> forElement(@NotNull PsiElement psiElement) {
114            return elementsCache.getDiagnostics(psiElement);
115        }
116    
117        @Override
118        public boolean isEmpty() {
119            return all().isEmpty();
120        }
121    
122        private boolean isSuppressed(@NotNull Diagnostic diagnostic) {
123            PsiElement element = diagnostic.getPsiElement();
124    
125            // If diagnostics are reported in a synthetic file generated by KtPsiFactory (dummy.kt),
126            // there's no point to present such diagnostics to the user, because the user didn't write this code
127            PsiFile file = element.getContainingFile();
128            if (file instanceof KtFile) {
129                if (KtPsiFactoryKt.getDoNotAnalyze((KtFile) file) != null) return true;
130            }
131    
132            for (DiagnosticSuppressor suppressor : DIAGNOSTIC_SUPPRESSORS.get()) {
133                if (suppressor.isSuppressed(diagnostic)) return true;
134            }
135    
136            KtAnnotated annotated = KtStubbedPsiUtil.getPsiOrStubParent(element, KtAnnotated.class, false);
137            if (annotated == null) return false;
138    
139            return isSuppressedByAnnotated(diagnostic, annotated, 0);
140        }
141    
142        /*
143           The cache is optimized for the case where no warnings are suppressed (most frequent one)
144    
145           trait Root {
146             suppress("X")
147             trait A {
148               trait B {
149                 suppress("Y")
150                 trait C {
151                   fun foo() = warning
152                 }
153               }
154             }
155           }
156    
157           Nothing is suppressed at foo, so we look above. While looking above we went up to the root (once) and propagated
158           all the suppressors down, so now we have:
159    
160              foo  - suppress(Y) from C
161              C    - suppress(Y) from C
162              B    - suppress(X) from A
163              A    - suppress(X) from A
164              Root - suppress() from Root
165    
166           Next time we look up anything under foo, we try the Y-suppressor and then immediately the X-suppressor, then to the empty
167           suppressor at the root. All the intermediate empty nodes are skipped, because every suppressor remembers its definition point.
168    
169           This way we need no more lookups than the number of suppress() annotations from here to the root.
170         */
171        private boolean isSuppressedByAnnotated(@NotNull Diagnostic diagnostic, @NotNull KtAnnotated annotated, int debugDepth) {
172            if (LOG.isDebugEnabled()) {
173                LOG.debug("Annotated: ", annotated.getName());
174                LOG.debug("Depth: ", debugDepth);
175                LOG.debug("Cache size: ", suppressors.size(), "\n");
176            }
177    
178            Suppressor suppressor = getOrCreateSuppressor(annotated);
179            if (suppressor.isSuppressed(diagnostic)) return true;
180    
181            KtAnnotated annotatedAbove = KtStubbedPsiUtil.getPsiOrStubParent(suppressor.getAnnotatedElement(), KtAnnotated.class, true);
182            if (annotatedAbove == null) return false;
183    
184            boolean suppressed = isSuppressedByAnnotated(diagnostic, annotatedAbove, debugDepth + 1);
185            Suppressor suppressorAbove = suppressors.get(annotatedAbove);
186            if (suppressorAbove != null && suppressorAbove.dominates(suppressor)) {
187                suppressors.put(annotated, suppressorAbove);
188            }
189    
190            return suppressed;
191        }
192    
193        @NotNull
194        private Suppressor getOrCreateSuppressor(@NotNull KtAnnotated annotated) {
195            Suppressor suppressor = suppressors.get(annotated);
196            if (suppressor == null) {
197                Set<String> strings = getSuppressingStrings(annotated);
198                if (strings.isEmpty()) {
199                    suppressor = new EmptySuppressor(annotated);
200                }
201                else if (strings.size() == 1) {
202                    suppressor = new SingularSuppressor(annotated, strings.iterator().next());
203                }
204                else {
205                    suppressor = new MultiSuppressor(annotated, strings);
206                }
207                suppressors.put(annotated, suppressor);
208            }
209            return suppressor;
210        }
211    
212        private Set<String> getSuppressingStrings(@NotNull KtAnnotated annotated) {
213            ImmutableSet.Builder<String> builder = ImmutableSet.builder();
214    
215            DeclarationDescriptor descriptor = context.get(BindingContext.DECLARATION_TO_DESCRIPTOR, annotated);
216    
217            if (descriptor != null) {
218                for (AnnotationDescriptor annotationDescriptor : descriptor.getAnnotations()) {
219                    processAnnotation(builder, annotationDescriptor);
220                }
221            }
222            else {
223                for (KtAnnotationEntry annotationEntry : annotated.getAnnotationEntries()) {
224                    AnnotationDescriptor annotationDescriptor = context.get(BindingContext.ANNOTATION, annotationEntry);
225                    processAnnotation(builder, annotationDescriptor);
226                }
227            }
228            return builder.build();
229        }
230    
231        private void processAnnotation(ImmutableSet.Builder<String> builder, AnnotationDescriptor annotationDescriptor) {
232            if (annotationDescriptor == null) return;
233    
234            for (SuppressStringProvider suppressStringProvider : ADDITIONAL_SUPPRESS_STRING_PROVIDERS.get()) {
235                builder.addAll(suppressStringProvider.get(annotationDescriptor));
236            }
237    
238            if (!KotlinBuiltIns.isSuppressAnnotation(annotationDescriptor)) return;
239    
240            // We only add strings and skip other values to facilitate recovery in presence of erroneous code
241            for (ConstantValue<?> arrayValue : annotationDescriptor.getAllValueArguments().values()) {
242                if ((arrayValue instanceof ArrayValue)) {
243                    for (ConstantValue<?> value : ((ArrayValue) arrayValue).getValue()) {
244                        if (value instanceof StringValue) {
245                            builder.add(String.valueOf(((StringValue) value).getValue()).toLowerCase());
246                        }
247                    }
248                }
249            }
250        }
251    
252        public static boolean isSuppressedByStrings(@NotNull Diagnostic diagnostic, @NotNull Set<String> strings) {
253            if (strings.contains("warnings") && diagnostic.getSeverity() == Severity.WARNING) return true;
254    
255            return strings.contains(diagnostic.getFactory().getName().toLowerCase());
256        }
257    
258        @NotNull
259        @Override
260        public ModificationTracker getModificationTracker() {
261            throw new IllegalStateException("Trying to obtain modification tracker for readonly DiagnosticsWithSuppression.");
262        }
263    
264        private static abstract class Suppressor {
265            private final KtAnnotated annotated;
266    
267            protected Suppressor(@NotNull KtAnnotated annotated) {
268                this.annotated = annotated;
269            }
270    
271            @NotNull
272            public KtAnnotated getAnnotatedElement() {
273                return annotated;
274            }
275    
276            public abstract boolean isSuppressed(@NotNull Diagnostic diagnostic);
277    
278            // true is \forall x. other.isSuppressed(x) -> this.isSuppressed(x)
279            public abstract boolean dominates(@NotNull Suppressor other);
280        }
281    
282        private static class EmptySuppressor extends Suppressor {
283    
284            private EmptySuppressor(@NotNull KtAnnotated annotated) {
285                super(annotated);
286            }
287    
288            @Override
289            public boolean isSuppressed(@NotNull Diagnostic diagnostic) {
290                return false;
291            }
292    
293            @Override
294            public boolean dominates(@NotNull Suppressor other) {
295                return other instanceof EmptySuppressor;
296            }
297        }
298    
299        private static class SingularSuppressor extends Suppressor {
300            private final String string;
301    
302            private SingularSuppressor(@NotNull KtAnnotated annotated, @NotNull String string) {
303                super(annotated);
304                this.string = string;
305            }
306    
307            @Override
308            public boolean isSuppressed(@NotNull Diagnostic diagnostic) {
309                return isSuppressedByStrings(diagnostic, ImmutableSet.of(string));
310            }
311    
312            @Override
313            public boolean dominates(@NotNull Suppressor other) {
314                return other instanceof EmptySuppressor
315                       || (other instanceof SingularSuppressor && ((SingularSuppressor) other).string.equals(string));
316            }
317        }
318    
319        private static class MultiSuppressor extends Suppressor {
320            private final Set<String> strings;
321    
322            private MultiSuppressor(@NotNull KtAnnotated annotated, @NotNull Set<String> strings) {
323                super(annotated);
324                this.strings = strings;
325            }
326    
327            @Override
328            public boolean isSuppressed(@NotNull Diagnostic diagnostic) {
329                return isSuppressedByStrings(diagnostic, strings);
330            }
331    
332            @Override
333            public boolean dominates(@NotNull Suppressor other) {
334                // it's too costly to check set inclusion
335                return other instanceof EmptySuppressor;
336            }
337        }
338    
339        @TestOnly
340        @NotNull
341        public Collection<Diagnostic> getDiagnostics() {
342            return diagnostics;
343        }
344    }