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.java.resolver;
018    
019    import com.intellij.openapi.diagnostic.Logger;
020    import com.intellij.openapi.vfs.VirtualFile;
021    import org.jetbrains.annotations.NotNull;
022    import org.jetbrains.annotations.Nullable;
023    import org.jetbrains.asm4.*;
024    import org.jetbrains.asm4.commons.Method;
025    import org.jetbrains.jet.descriptors.serialization.JavaProtoBufUtil;
026    import org.jetbrains.jet.descriptors.serialization.NameResolver;
027    import org.jetbrains.jet.descriptors.serialization.ProtoBuf;
028    import org.jetbrains.jet.descriptors.serialization.descriptors.AnnotationDeserializer;
029    import org.jetbrains.jet.lang.descriptors.*;
030    import org.jetbrains.jet.lang.descriptors.annotations.AnnotationDescriptor;
031    import org.jetbrains.jet.lang.resolve.DescriptorUtils;
032    import org.jetbrains.jet.lang.resolve.constants.CompileTimeConstant;
033    import org.jetbrains.jet.lang.resolve.constants.EnumValue;
034    import org.jetbrains.jet.lang.resolve.constants.ErrorValue;
035    import org.jetbrains.jet.lang.resolve.java.JvmAbi;
036    import org.jetbrains.jet.lang.resolve.java.JvmAnnotationNames;
037    import org.jetbrains.jet.lang.resolve.java.PackageClassUtils;
038    import org.jetbrains.jet.lang.resolve.java.vfilefinder.VirtualFileFinder;
039    import org.jetbrains.jet.lang.resolve.lazy.storage.LockBasedStorageManager;
040    import org.jetbrains.jet.lang.resolve.lazy.storage.MemoizedFunctionToNotNull;
041    import org.jetbrains.jet.lang.resolve.lazy.storage.StorageManager;
042    import org.jetbrains.jet.lang.resolve.name.FqName;
043    import org.jetbrains.jet.lang.resolve.name.Name;
044    import org.jetbrains.jet.lang.types.ErrorUtils;
045    
046    import javax.inject.Inject;
047    import java.io.IOException;
048    import java.util.*;
049    
050    import static org.jetbrains.asm4.ClassReader.*;
051    import static org.jetbrains.jet.lang.resolve.java.DescriptorSearchRule.IGNORE_KOTLIN_SOURCES;
052    import static org.jetbrains.jet.lang.resolve.java.resolver.DeserializedResolverUtils.kotlinFqNameToJavaFqName;
053    import static org.jetbrains.jet.lang.resolve.java.resolver.DeserializedResolverUtils.naiveKotlinFqName;
054    
055    public class AnnotationDescriptorDeserializer implements AnnotationDeserializer {
056        private static final Logger LOG = Logger.getInstance(AnnotationDescriptorDeserializer.class);
057    
058        private JavaClassResolver javaClassResolver;
059    
060        private VirtualFileFinder virtualFileFinder;
061    
062        // TODO: a single instance of StorageManager for all computations in resolve-java
063        private final LockBasedStorageManager storageManager = new LockBasedStorageManager();
064    
065        private final MemoizedFunctionToNotNull<VirtualFile, Map<MemberSignature, List<AnnotationDescriptor>>> memberAnnotations =
066                storageManager.createMemoizedFunction(
067                        new MemoizedFunctionToNotNull<VirtualFile, Map<MemberSignature, List<AnnotationDescriptor>>>() {
068                            @NotNull
069                            @Override
070                            public Map<MemberSignature, List<AnnotationDescriptor>> fun(@NotNull VirtualFile file) {
071                                try {
072                                    return loadMemberAnnotationsFromFile(file);
073                                }
074                                catch (IOException e) {
075                                    LOG.error("Error loading member annotations from file: " + file, e);
076                                    return Collections.emptyMap();
077                                }
078                            }
079                        }, StorageManager.ReferenceKind.STRONG);
080    
081        @Inject
082        public void setVirtualFileFinder(VirtualFileFinder virtualFileFinder) {
083            this.virtualFileFinder = virtualFileFinder;
084        }
085    
086        @Inject
087        public void setJavaClassResolver(JavaClassResolver javaClassResolver) {
088            this.javaClassResolver = javaClassResolver;
089        }
090    
091        @NotNull
092        @Override
093        public List<AnnotationDescriptor> loadClassAnnotations(@NotNull ClassDescriptor descriptor, @NotNull ProtoBuf.Class classProto) {
094            VirtualFile virtualFile = findVirtualFileByClass(descriptor);
095            if (virtualFile == null) {
096                // This means that the resource we're constructing the descriptor from is no longer present: VirtualFileFinder had found the
097                // file earlier, but it can't now
098                LOG.error("Virtual file for loading class annotations is not found: " + descriptor);
099                return Collections.emptyList();
100            }
101            try {
102                return loadClassAnnotationsFromFile(virtualFile);
103            }
104            catch (IOException e) {
105                LOG.error("Error loading member annotations from file: " + virtualFile, e);
106                return Collections.emptyList();
107            }
108        }
109    
110        @Nullable
111        private VirtualFile findVirtualFileByDescriptor(@NotNull ClassOrNamespaceDescriptor descriptor) {
112            if (descriptor instanceof ClassDescriptor) {
113                return findVirtualFileByClass((ClassDescriptor) descriptor);
114            }
115            else if (descriptor instanceof NamespaceDescriptor) {
116                return findVirtualFileByPackage((NamespaceDescriptor) descriptor);
117            }
118            else {
119                throw new IllegalStateException("Unrecognized descriptor: " + descriptor);
120            }
121        }
122    
123        @Nullable
124        private VirtualFile findVirtualFileByClass(@NotNull ClassDescriptor descriptor) {
125            return virtualFileFinder.find(kotlinFqNameToJavaFqName(naiveKotlinFqName(descriptor)));
126        }
127    
128        @Nullable
129        private VirtualFile findVirtualFileByPackage(@NotNull NamespaceDescriptor descriptor) {
130            return virtualFileFinder.find(PackageClassUtils.getPackageClassFqName(DescriptorUtils.getFQName(descriptor).toSafe()));
131        }
132    
133        @NotNull
134        private List<AnnotationDescriptor> loadClassAnnotationsFromFile(@NotNull VirtualFile virtualFile) throws IOException {
135            final List<AnnotationDescriptor> result = new ArrayList<AnnotationDescriptor>();
136    
137            new ClassReader(virtualFile.getInputStream()).accept(new ClassVisitor(Opcodes.ASM4) {
138                @Override
139                public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
140                    return resolveAnnotation(desc, result);
141                }
142            }, SKIP_CODE | SKIP_DEBUG | SKIP_FRAMES);
143    
144            return result;
145        }
146    
147        private static boolean ignoreAnnotation(@NotNull String desc) {
148            // TODO: JvmAbi.JETBRAINS_NOT_NULL_ANNOTATION ?
149            return desc.equals(JvmAnnotationNames.KOTLIN_CLASS.getDescriptor())
150                   || desc.equals(JvmAnnotationNames.KOTLIN_PACKAGE.getDescriptor())
151                   || desc.startsWith("Ljet/runtime/typeinfo/");
152        }
153    
154        @NotNull
155        private static FqName convertJvmDescriptorToFqName(@NotNull String desc) {
156            assert desc.startsWith("L") && desc.endsWith(";") : "Not a JVM descriptor: " + desc;
157            String fqName = desc.substring(1, desc.length() - 1).replace('$', '.').replace('/', '.');
158            return new FqName(fqName);
159        }
160    
161        @Nullable
162        private AnnotationVisitor resolveAnnotation(@NotNull String desc, @NotNull final List<AnnotationDescriptor> result) {
163            if (ignoreAnnotation(desc)) return null;
164    
165            FqName annotationFqName = convertJvmDescriptorToFqName(desc);
166            final ClassDescriptor annotationClass = resolveAnnotationClass(annotationFqName);
167            final AnnotationDescriptor annotation = new AnnotationDescriptor();
168            annotation.setAnnotationType(annotationClass.getDefaultType());
169    
170            return new AnnotationVisitor(Opcodes.ASM4) {
171                // TODO: arrays, annotations, java.lang.Class
172                @Override
173                public void visit(String name, Object value) {
174                    CompileTimeConstant<?> argument = JavaAnnotationArgumentResolver.resolveCompileTimeConstantValue(value, null);
175                    setArgumentValueByName(name, argument != null ? argument : new ErrorValue("Unsupported annotation argument: " + name));
176                }
177    
178                @Override
179                public void visitEnum(String name, String desc, String value) {
180                    FqName fqName = convertJvmDescriptorToFqName(desc);
181                    setArgumentValueByName(name, enumEntryValue(fqName, Name.identifier(value)));
182                }
183    
184                @NotNull
185                private CompileTimeConstant<?> enumEntryValue(@NotNull FqName enumFqName, @NotNull Name name) {
186                    ClassDescriptor enumClass = javaClassResolver.resolveClass(enumFqName, IGNORE_KOTLIN_SOURCES);
187                    if (enumClass != null && enumClass.getKind() == ClassKind.ENUM_CLASS) {
188                        ClassDescriptor classObject = enumClass.getClassObjectDescriptor();
189                        if (classObject != null) {
190                            Collection<VariableDescriptor> properties = classObject.getDefaultType().getMemberScope().getProperties(name);
191                            if (properties.size() == 1) {
192                                VariableDescriptor property = properties.iterator().next();
193                                if (property instanceof PropertyDescriptor) {
194                                    return new EnumValue((PropertyDescriptor) property);
195                                }
196                            }
197                        }
198                    }
199                    return new ErrorValue("Unresolved enum entry: " + enumFqName + "." + name);
200                }
201    
202                @Override
203                public void visitEnd() {
204                    result.add(annotation);
205                }
206    
207                private void setArgumentValueByName(@NotNull String name, @NotNull CompileTimeConstant<?> argumentValue) {
208                    ValueParameterDescriptor parameter =
209                            DescriptorResolverUtils.getAnnotationParameterByName(Name.identifier(name), annotationClass);
210                    if (parameter != null) {
211                        annotation.setValueArgument(parameter, argumentValue);
212                    }
213                }
214            };
215        }
216    
217        @NotNull
218        private ClassDescriptor resolveAnnotationClass(@NotNull FqName fqName) {
219            ClassDescriptor annotationClass = javaClassResolver.resolveClass(fqName, IGNORE_KOTLIN_SOURCES);
220            return annotationClass != null ? annotationClass : ErrorUtils.getErrorClass();
221        }
222    
223        @NotNull
224        @Override
225        public List<AnnotationDescriptor> loadCallableAnnotations(
226                @NotNull ClassOrNamespaceDescriptor container,
227                @NotNull ProtoBuf.Callable proto,
228                @NotNull NameResolver nameResolver,
229                @NotNull AnnotatedCallableKind kind
230        ) {
231            MemberSignature signature = getCallableSignature(proto, nameResolver, kind);
232            if (signature == null) return Collections.emptyList();
233    
234            VirtualFile file = getVirtualFileWithMemberAnnotations(container, proto, nameResolver);
235            if (file == null) {
236                LOG.error("Virtual file for loading member annotations is not found: " + container);
237            }
238    
239            List<AnnotationDescriptor> annotations = memberAnnotations.fun(file).get(signature);
240            return annotations == null ? Collections.<AnnotationDescriptor>emptyList() : annotations;
241        }
242    
243        @Nullable
244        private VirtualFile getVirtualFileWithMemberAnnotations(
245                @NotNull ClassOrNamespaceDescriptor container,
246                @NotNull ProtoBuf.Callable proto,
247                @NotNull NameResolver nameResolver
248        ) {
249            if (container instanceof NamespaceDescriptor) {
250                Name name = JavaProtoBufUtil.loadSrcClassName(proto, nameResolver);
251                if (name != null) {
252                    // To locate a package$src class, we first find the facade virtual file (*Package.class) and then look up the $src file in
253                    // the same directory. This hack is needed because FileManager doesn't find classfiles for $src classes
254                    VirtualFile facadeFile = findVirtualFileByPackage((NamespaceDescriptor) container);
255                    if (facadeFile != null) {
256                        VirtualFile srcFile = facadeFile.getParent().findChild(name + ".class");
257                        if (srcFile != null) {
258                            return srcFile;
259                        }
260                    }
261                }
262                return null;
263            }
264            else if (container instanceof ClassDescriptor && ((ClassDescriptor) container).getKind() == ClassKind.CLASS_OBJECT) {
265                // Backing fields of properties of a class object are generated in the outer class
266                if (JavaProtoBufUtil.isStaticFieldInOuter(proto)) {
267                    return findVirtualFileByDescriptor((ClassOrNamespaceDescriptor) container.getContainingDeclaration());
268                }
269            }
270    
271            return findVirtualFileByDescriptor(container);
272        }
273    
274        @Nullable
275        private static MemberSignature getCallableSignature(
276                @NotNull ProtoBuf.Callable proto,
277                @NotNull NameResolver nameResolver,
278                @NotNull AnnotatedCallableKind kind
279        ) {
280            switch (kind) {
281                case FUNCTION:
282                    return MemberSignature.fromMethod(JavaProtoBufUtil.loadMethodSignature(proto, nameResolver));
283                case PROPERTY_GETTER:
284                    return MemberSignature.fromMethod(JavaProtoBufUtil.loadPropertyGetterSignature(proto, nameResolver));
285                case PROPERTY_SETTER:
286                    return MemberSignature.fromMethod(JavaProtoBufUtil.loadPropertySetterSignature(proto, nameResolver));
287                case PROPERTY:
288                    JavaProtoBufUtil.PropertyData data = JavaProtoBufUtil.loadPropertyData(proto, nameResolver);
289                    return data == null ? null :
290                           MemberSignature.fromPropertyData(data.getFieldType(), data.getFieldName(), data.getSyntheticMethodName());
291                default:
292                    return null;
293            }
294        }
295    
296        @NotNull
297        private Map<MemberSignature, List<AnnotationDescriptor>> loadMemberAnnotationsFromFile(@NotNull VirtualFile file) throws IOException {
298            final Map<MemberSignature, List<AnnotationDescriptor>> memberAnnotations =
299                    new HashMap<MemberSignature, List<AnnotationDescriptor>>();
300    
301            new ClassReader(file.getInputStream()).accept(new ClassVisitor(Opcodes.ASM4) {
302                @Override
303                public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
304                    final MemberSignature methodSignature = MemberSignature.fromMethodNameAndDesc(name, desc);
305                    final List<AnnotationDescriptor> result = new ArrayList<AnnotationDescriptor>();
306    
307                    return new MethodVisitor(Opcodes.ASM4) {
308                        @Override
309                        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
310                            return resolveAnnotation(desc, result);
311                        }
312    
313                        @Override
314                        public void visitEnd() {
315                            if (!result.isEmpty()) {
316                                memberAnnotations.put(methodSignature, result);
317                            }
318                        }
319                    };
320                }
321    
322                @Override
323                public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
324                    final MemberSignature fieldSignature = MemberSignature.fromFieldNameAndDesc(name, desc);
325                    final List<AnnotationDescriptor> result = new ArrayList<AnnotationDescriptor>();
326    
327                    return new FieldVisitor(Opcodes.ASM4) {
328                        @Override
329                        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
330                            return resolveAnnotation(desc, result);
331                        }
332    
333                        @Override
334                        public void visitEnd() {
335                            if (!result.isEmpty()) {
336                                memberAnnotations.put(fieldSignature, result);
337                            }
338                        }
339                    };
340                }
341            }, SKIP_CODE | SKIP_DEBUG | SKIP_FRAMES);
342    
343            return memberAnnotations;
344        }
345    
346        // The purpose of this class is to hold a unique signature of either a method or a field, so that annotations on a member can be put
347        // into a map indexed by these signatures
348        private static final class MemberSignature {
349            private final String signature;
350    
351            private MemberSignature(@NotNull String signature) {
352                this.signature = signature;
353            }
354    
355            @Nullable
356            public static MemberSignature fromPropertyData(
357                    @Nullable Type fieldType,
358                    @Nullable String fieldName,
359                    @Nullable String syntheticMethodName
360            ) {
361                if (fieldName != null && fieldType != null) {
362                    return fromFieldNameAndDesc(fieldName, fieldType.getDescriptor());
363                }
364                else if (syntheticMethodName != null) {
365                    return fromMethodNameAndDesc(syntheticMethodName, JvmAbi.ANNOTATED_PROPERTY_METHOD_SIGNATURE);
366                }
367                else {
368                    return null;
369                }
370            }
371    
372            @Nullable
373            public static MemberSignature fromMethod(@Nullable Method method) {
374                return method == null ? null : fromMethodNameAndDesc(method.getName(), method.getDescriptor());
375            }
376    
377            @NotNull
378            public static MemberSignature fromMethodNameAndDesc(@NotNull String name, @NotNull String desc) {
379                return new MemberSignature(name + desc);
380            }
381    
382            @NotNull
383            public static MemberSignature fromFieldNameAndDesc(@NotNull String name, @NotNull String desc) {
384                return new MemberSignature(name + "#" + desc);
385            }
386    
387            @Override
388            public int hashCode() {
389                return signature.hashCode();
390            }
391    
392            @Override
393            public boolean equals(Object o) {
394                return o instanceof MemberSignature && signature.equals(((MemberSignature) o).signature);
395            }
396    
397            @Override
398            public String toString() {
399                return signature;
400            }
401        }
402    
403        @NotNull
404        @Override
405        public List<AnnotationDescriptor> loadValueParameterAnnotations(@NotNull ProtoBuf.Callable.ValueParameter parameterProto) {
406            throw new UnsupportedOperationException(); // TODO
407        }
408    }