blob: ea215673b719165f0044ef1020cff9675f45af2a [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.jps.intellilang.instrumentation;
import com.intellij.compiler.instrumentation.InstrumentationClassFinder;
import com.intellij.openapi.util.Ref;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.intellilang.model.InstrumentationException;
import org.jetbrains.org.objectweb.asm.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.regex.Pattern;
class PatternInstrumenter extends ClassVisitor implements Opcodes {
@NonNls static final String PATTERN_CACHE_NAME = "$_PATTERN_CACHE_$";
@NonNls static final String ASSERTIONS_DISABLED_NAME = "$assertionsDisabled";
@NonNls static final String JAVA_LANG_STRING = "Ljava/lang/String;";
@NonNls static final String JAVA_UTIL_REGEX_PATTERN = "[Ljava/util/regex/Pattern;";
private boolean myHasAssertions;
private boolean myHasStaticInitializer;
private final LinkedHashSet<String> myPatterns = new LinkedHashSet<String>();
private final String myPatternAnnotationClassName;
final InstrumentationType myInstrumentationType;
private final InstrumentationClassFinder myClassFinder;
private final Map<String, String> myAnnotationNameToPatternMap = new HashMap<String, String>(); // can contain null values!
private final Set<String> myProcessedAnnotations = new HashSet<String>(); // checked annotation classes
String myClassName;
private boolean myInstrumented;
private RuntimeException myPostponedError;
boolean myIsNonStaticInnerClass;
public PatternInstrumenter(@NotNull String patternAnnotationClassName, ClassVisitor classvisitor,
InstrumentationType instrumentation,
InstrumentationClassFinder classFinder) {
super(Opcodes.ASM5, classvisitor);
myPatternAnnotationClassName = patternAnnotationClassName;
myInstrumentationType = instrumentation;
myClassFinder = classFinder;
// initial setup: null value means we should discover the pattern string 'inplace'
myAnnotationNameToPatternMap.put(patternAnnotationClassName, null);
myProcessedAnnotations.add(patternAnnotationClassName);
}
public boolean instrumented() {
return myInstrumented;
}
void markInstrumented() {
myInstrumented = true;
processPostponedErrors();
}
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
myClassName = name;
}
public void visitInnerClass(String name, String outerName, String innerName, int access) {
super.visitInnerClass(name, outerName, innerName, access);
if (myClassName.equals(name)) {
myIsNonStaticInnerClass = (access & ACC_STATIC) == 0;
}
}
public FieldVisitor visitField(final int access, final String name, final String desc, final String signature, final Object value) {
if (name.equals(ASSERTIONS_DISABLED_NAME)) {
myHasAssertions = true;
}
else if (name.equals(PATTERN_CACHE_NAME)) {
throw new InstrumentationException("Error: Processing an already instrumented class: " + myClassName + ". Please recompile the affected class(es) or rebuild the project.");
}
return super.visitField(access, name, desc, signature, value);
}
public void visitEnd() {
if (myInstrumented) {
addField(PATTERN_CACHE_NAME, ACC_PRIVATE + ACC_FINAL + ACC_STATIC + ACC_SYNTHETIC, JAVA_UTIL_REGEX_PATTERN);
if (myInstrumentationType == InstrumentationType.ASSERT) {
if (!myHasAssertions) {
addField(ASSERTIONS_DISABLED_NAME, ACC_FINAL + ACC_STATIC + ACC_SYNTHETIC, "Z");
}
}
if (!myHasStaticInitializer) {
createStaticInitializer();
}
}
super.visitEnd();
}
private void addField(String name, int modifiers, String type) {
final FieldVisitor fv = cv.visitField(modifiers, name, type, null, null);
fv.visitEnd();
}
private void createStaticInitializer() {
final MethodVisitor mv = cv.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
patchStaticInitializer(mv);
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
private void patchStaticInitializer(MethodVisitor mv) {
if (!myHasAssertions && myInstrumentationType == InstrumentationType.ASSERT) {
initAssertions(mv);
}
initPatterns(mv);
}
// verify pattern and add compiled pattern to static cache
private void initPatterns(MethodVisitor mv) {
mv.visitIntInsn(BIPUSH, myPatterns.size());
mv.visitTypeInsn(ANEWARRAY, "java/util/regex/Pattern");
mv.visitFieldInsn(PUTSTATIC, myClassName, PATTERN_CACHE_NAME, JAVA_UTIL_REGEX_PATTERN);
int i = 0;
for (String pattern : myPatterns) {
// check the pattern so we can rely on the pattern being valid at runtime
try {
Pattern.compile(pattern);
}
catch (Exception e) {
throw new InstrumentationException("Illegal Pattern: " + pattern, e);
}
mv.visitFieldInsn(GETSTATIC, myClassName, PATTERN_CACHE_NAME, JAVA_UTIL_REGEX_PATTERN);
mv.visitIntInsn(BIPUSH, i++);
mv.visitLdcInsn(pattern);
mv.visitMethodInsn(INVOKESTATIC, "java/util/regex/Pattern", "compile", "(Ljava/lang/String;)Ljava/util/regex/Pattern;", false);
mv.visitInsn(AASTORE);
}
}
// add assert startup code
private void initAssertions(MethodVisitor mv) {
mv.visitLdcInsn(Type.getType("L" + myClassName + ";"));
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "desiredAssertionStatus", "()Z", false);
Label l0 = new Label();
mv.visitJumpInsn(IFNE, l0);
mv.visitInsn(ICONST_1);
Label l1 = new Label();
mv.visitJumpInsn(GOTO, l1);
mv.visitLabel(l0);
mv.visitInsn(ICONST_0);
mv.visitLabel(l1);
mv.visitFieldInsn(PUTSTATIC, myClassName, ASSERTIONS_DISABLED_NAME, "Z");
}
public MethodVisitor visitMethod(final int access, final String name, String desc, String signature, String[] exceptions) {
final MethodVisitor methodvisitor = cv.visitMethod(access, name, desc, signature, exceptions);
// patch static initializer
if ((access & ACC_STATIC) != 0 && name.equals("<clinit>")) {
myHasStaticInitializer = true;
return new ErrorPostponingMethodVisitor(this, name, methodvisitor) {
public void visitCode() {
super.visitCode();
patchStaticInitializer(mv);
}
};
}
final Type[] argTypes = Type.getArgumentTypes(desc);
final Type returnType = Type.getReturnType(desc);
// don't dig through the whole method if there's nothing to do in it
if (isStringType(returnType)) {
return new InstrumentationAdapter(this, methodvisitor, argTypes, returnType, access, name);
}
else {
for (Type type : argTypes) {
if (isStringType(type)) {
return new InstrumentationAdapter(this, methodvisitor, argTypes, returnType, access, name);
}
}
}
return new ErrorPostponingMethodVisitor(this, name, methodvisitor);
}
private static boolean isStringType(Type type) {
return type.getSort() == Type.OBJECT && type.getDescriptor().equals(JAVA_LANG_STRING);
}
public int addPattern(String s) {
if (myPatterns.add(s)) {
return myPatterns.size() - 1;
}
return Arrays.asList(myPatterns.toArray()).indexOf(s);
}
public boolean acceptAnnotation(String annotationClassName) {
if (annotationClassName == null) {
// unfortunately sometimes ASM may return null values
return false;
}
processAnnotation(annotationClassName);
return myAnnotationNameToPatternMap.containsKey(annotationClassName);
}
/**
* @param annotationClassname
* @return pattern string for 'alias' annotations, as specified in the 'base' annotation,
* otherwise null, (for the 'base' annotation class name null is returned as well)
*/
@Nullable
public String getAnnotationPattern(String annotationClassName) {
processAnnotation(annotationClassName);
return myAnnotationNameToPatternMap.get(annotationClassName);
}
private void processAnnotation(String annotationClassName) {
if (!myProcessedAnnotations.add(annotationClassName)) {
return;
}
try {
final InputStream is = myClassFinder.getClassBytesAsStream(annotationClassName);
if (is != null) {
try {
final Ref<String> patternString = new Ref<String>(null);
// dig into annotation class and check if it is annotated with pattern annotation.
// if yes, load the pattern string from the pattern annotation and associate it with this annotation
final ClassVisitor visitor = new ClassVisitor(Opcodes.ASM5) {
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if (patternString.get() != null || !myPatternAnnotationClassName.equals(Type.getType(desc).getClassName())) {
return null; // already found or is not pattern annotation
}
// dig into pattern annotation in order to discover the pattern string
return new AnnotationVisitor(Opcodes.ASM5) {
public void visit(@NonNls String name, Object value) {
if ("value".equals(name) && value instanceof String) {
patternString.set((String)value);
}
}
};
}
};
new ClassReader(is).accept(visitor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
final String pattern = patternString.get();
if (pattern != null) {
myAnnotationNameToPatternMap.put(annotationClassName, pattern);
}
}
finally {
is.close();
}
}
}
catch (IOException ignored) {
// todo
}
}
void registerError(String methodName, String operationName, Throwable e) {
if (myPostponedError == null) {
// throw the first error that occurred
Throwable err = e.getCause();
if (err == null) {
err = e;
}
myPostponedError = new RuntimeException("Operation '" + operationName + "' failed for " + myClassName + "." + methodName + "(): " + err.getMessage(), err);
}
if (myInstrumented) {
processPostponedErrors();
}
}
private void processPostponedErrors() {
final RuntimeException error = myPostponedError;
if (error != null) {
throw error;
}
}
}