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 }