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 }