PY-16920 delegated methods in custom type were not filtered
[idea/community.git] / python / src / com / jetbrains / python / PyCustomType.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.jetbrains.python;
17
18 import com.google.common.base.Preconditions;
19 import com.google.common.base.Predicate;
20 import com.google.common.collect.Collections2;
21 import com.intellij.codeInsight.lookup.LookupElement;
22 import com.intellij.openapi.util.text.StringUtil;
23 import com.intellij.psi.PsiElement;
24 import com.intellij.util.ProcessingContext;
25 import com.intellij.util.Processor;
26 import com.jetbrains.NotNullPredicate;
27 import com.jetbrains.python.psi.*;
28 import com.jetbrains.python.psi.resolve.PyResolveContext;
29 import com.jetbrains.python.psi.resolve.RatedResolveResult;
30 import com.jetbrains.python.psi.types.*;
31 import org.jetbrains.annotations.NotNull;
32 import org.jetbrains.annotations.Nullable;
33
34 import java.util.*;
35
36 /**
37  * Custom (aka dynamic) type that delegates calls to some classes you pass to it.
38  * We say this this class <strong>mimics</strong> such classes.
39  * To be used for cases like "type()".
40  * It optionally filters methods using {@link Processor}
41  *
42  * @author Ilya.Kazakevich
43  */
44 public class PyCustomType implements PyClassLikeType {
45
46   @NotNull
47   private final List<PyClassLikeType> myTypesToMimic = new ArrayList<PyClassLikeType>();
48
49   @Nullable
50   private final Processor<PyElement> myFilter;
51
52   private final boolean myInstanceType;
53
54
55   /**
56    * @param filter       filter to filter methods from classes (may be null to do no filtering)
57    * @param instanceType if true, then this class implements instance (it reports it is not definition and returns "this
58    *                     for {@link #toInstance()} call). If false, <strong>calling this type creates similar type with instance=true</strong>
59    *                     (like ctor)
60    * @param typesToMimic types to "mimic": delegate calls to  (must be one at least!)
61    */
62   public PyCustomType(@Nullable final Processor<PyElement> filter,
63                       final boolean instanceType,
64                       @NotNull final PyClassLikeType... typesToMimic) {
65     Preconditions.checkArgument(typesToMimic.length > 0, "Provide at least one class");
66     myFilter = filter;
67     myTypesToMimic.addAll(Collections2.filter(Arrays.asList(typesToMimic), NotNullPredicate.INSTANCE));
68     myInstanceType = instanceType;
69   }
70
71   /**
72    * @return class we mimic (if any). Check class manual for more info.
73    */
74   @NotNull
75   public final List<PyClassLikeType> getTypesToMimic() {
76     return Collections.unmodifiableList(myTypesToMimic);
77   }
78
79   @Override
80   public final boolean isDefinition() {
81     return !myInstanceType;
82   }
83
84   @Override
85   public final PyClassLikeType toInstance() {
86     return myInstanceType
87            ? this
88            : new PyCustomType(myFilter, true, myTypesToMimic.toArray(new PyClassLikeType[myTypesToMimic.size()]));
89   }
90
91
92   @Nullable
93   @Override
94   public final String getClassQName() {
95     return null;
96   }
97
98   @NotNull
99   @Override
100   public final List<PyClassLikeType> getSuperClassTypes(@NotNull final TypeEvalContext context) {
101     return Collections.emptyList();
102   }
103
104   @Nullable
105   @Override
106   public final List<? extends RatedResolveResult> resolveMember(@NotNull final String name,
107                                                                 @Nullable final PyExpression location,
108                                                                 @NotNull final AccessDirection direction,
109                                                                 @NotNull final PyResolveContext resolveContext,
110                                                                 final boolean inherited) {
111     final List<RatedResolveResult> globalResult = new ArrayList<RatedResolveResult>();
112
113     // Delegate calls to classes, we mimic but filter if filter is set.
114     for (final PyClassLikeType typeToMimic : myTypesToMimic) {
115       final List<? extends RatedResolveResult> results = typeToMimic.resolveMember(name, location, direction, resolveContext, inherited);
116       if (results != null) {
117         globalResult.addAll(Collections2.filter(results, new ResolveFilter()));
118       }
119     }
120     return globalResult;
121   }
122
123   @Override
124   public final boolean isValid() {
125     for (final PyClassLikeType type : myTypesToMimic) {
126       if (!type.isValid()) {
127         return false;
128       }
129     }
130
131     return true;
132   }
133
134   @Nullable
135   @Override
136   public final PyClassLikeType getMetaClassType(@NotNull final TypeEvalContext context, final boolean inherited) {
137     return null;
138   }
139
140   @Override
141   public final boolean isCallable() {
142     if (!myInstanceType) {
143       return true; // Due to ctor
144     }
145     for (final PyClassLikeType typeToMimic : myTypesToMimic) {
146       if (typeToMimic.isCallable()) {
147         return true;
148       }
149     }
150
151     return false;
152   }
153
154   @Nullable
155   @Override
156   public final PyType getReturnType(@NotNull final TypeEvalContext context) {
157     return (myInstanceType ? null : toInstance());
158   }
159
160   @Nullable
161   @Override
162   public final PyType getCallType(@NotNull final TypeEvalContext context, @NotNull final PyCallSiteExpression callSite) {
163     return getReturnType(context);
164   }
165
166   @Nullable
167   @Override
168   public final List<PyCallableParameter> getParameters(@NotNull final TypeEvalContext context) {
169     return null;
170   }
171
172   @Nullable
173   @Override
174   public final List<? extends RatedResolveResult> resolveMember(@NotNull final String name,
175                                                                 @Nullable final PyExpression location,
176                                                                 @NotNull final AccessDirection direction,
177                                                                 @NotNull final PyResolveContext resolveContext) {
178     return resolveMember(name, location, direction, resolveContext, true);
179   }
180
181   @NotNull
182   @Override
183   public final List<PyClassLikeType> getAncestorTypes(@NotNull final TypeEvalContext context) {
184     final Collection<PyClassLikeType> result = new LinkedHashSet<PyClassLikeType>();
185     for (final PyClassLikeType type : myTypesToMimic) {
186       result.addAll(type.getAncestorTypes(context));
187     }
188
189     return new ArrayList<PyClassLikeType>(result);
190   }
191
192   @Override
193   public final Object[] getCompletionVariants(final String completionPrefix, final PsiElement location, final ProcessingContext context) {
194     final Collection<Object> lookupElements = new ArrayList<Object>();
195
196     for (final PyClassLikeType parentType : myTypesToMimic) {
197       lookupElements.addAll(Collections2.filter(Arrays.asList(parentType.getCompletionVariants(completionPrefix, location, context)),
198                                                 new CompletionFilter()));
199     }
200     return lookupElements.toArray(new Object[lookupElements.size()]);
201   }
202
203
204   @Nullable
205   @Override
206   public final String getName() {
207     final Collection<String> classNames = new ArrayList<String>(myTypesToMimic.size());
208     for (final PyClassLikeType type : myTypesToMimic) {
209       String name = type.getName();
210       if (name == null && (type instanceof PyClassType)) {
211         name = ((PyClassType)type).getPyClass().getName();
212       }
213       if (name != null) {
214         classNames.add(name);
215       }
216     }
217
218
219     return PyBundle.message("custom.type.mimic.name", StringUtil.join(classNames, ","));
220   }
221
222   @Override
223   public final boolean isBuiltin() {
224     return false;
225   }
226
227   @Override
228   public final void assertValid(final String message) {
229     for (final PyClassLikeType type : myTypesToMimic) {
230       type.assertValid(message);
231     }
232   }
233
234
235   /**
236    * Predicate that filters resolve candidates using {@link #myFilter}
237    */
238   private class ResolveFilter implements Predicate<RatedResolveResult> {
239     @Override
240     public final boolean apply(@Nullable final RatedResolveResult input) {
241       if (input == null) {
242         return false;
243       }
244       if (myFilter == null) {
245         return true; // No need to check
246       }
247       final PyElement pyElement = PyUtil.as(input.getElement(), PyElement.class);
248       if (pyElement == null) {
249         return false;
250       }
251       return myFilter.process(pyElement);
252     }
253   }
254
255   @Override
256   public final void visitMembers(@NotNull final Processor<PsiElement> processor, final boolean inherited, @NotNull final TypeEvalContext context) {
257     for (final PyClassLikeType type : myTypesToMimic) {
258       // Only visit methods that are allowed by filter (if any)
259       type.visitMembers(new Processor<PsiElement>() {
260         @Override
261         public boolean process(final PsiElement t) {
262           if (!(t instanceof PyElement)) {
263             return true;
264           }
265           if (myFilter == null || myFilter.process((PyElement)t)) {
266             return processor.process(t);
267           }
268           return true;
269         }
270       }, inherited, context);
271     }
272   }
273
274   /**
275    * Predicate that filters completion using {@link #myFilter}
276    */
277   private class CompletionFilter implements Predicate<Object> {
278     @Override
279     public final boolean apply(@Nullable final Object input) {
280       if (input == null) {
281         return false;
282       }
283       if (myFilter == null) {
284         return true; // No need to check
285       }
286       if (!(input instanceof LookupElement)) {
287         return true; // Do not know how to check
288       }
289       final PyElement pyElement = PyUtil.as(((LookupElement)input).getPsiElement(), PyElement.class);
290       if (pyElement == null) {
291         return false;
292       }
293       return myFilter.process(pyElement);
294     }
295   }
296 }