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