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