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