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 }