diff --git a/.idea/modules.xml b/.idea/modules.xml index 94ca5a1252c3..6443efd723a0 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -63,6 +63,7 @@ + diff --git a/build/scripts/layouts.gant b/build/scripts/layouts.gant index c97c321eeeb1..32e1658a0b3a 100644 --- a/build/scripts/layouts.gant +++ b/build/scripts/layouts.gant @@ -192,6 +192,9 @@ def layoutFull(String home, String targetDirectory, String patchedDescriptorDir module("IntelliLang-xml") module("IntelliLang-javaee") } + jar("intellilang-jps-plugin.jar") { + module("intellilang-jps-plugin") + } } } } diff --git a/java/compiler/instrumentation-util/src/com/intellij/compiler/instrumentation/InstrumentationClassFinder.java b/java/compiler/instrumentation-util/src/com/intellij/compiler/instrumentation/InstrumentationClassFinder.java index 78898be2bb23..a5d56e4e632a 100644 --- a/java/compiler/instrumentation-util/src/com/intellij/compiler/instrumentation/InstrumentationClassFinder.java +++ b/java/compiler/instrumentation-util/src/com/intellij/compiler/instrumentation/InstrumentationClassFinder.java @@ -100,7 +100,25 @@ public class InstrumentationClassFinder { if (aClass != null) { return aClass; } - + + final InputStream is = getClassBytesAsStream(internalName); + + if (is == null) { + throw new ClassNotFoundException("Class not found: " + internalName); + } + + try { + final PseudoClass result = loadPseudoClass(is); + myLoaded.put(internalName, result); + return result; + } + finally { + is.close(); + } + } + + public InputStream getClassBytesAsStream(String className) throws IOException { + final String internalName = className.replace('.', '/'); // normalize InputStream is = null; // first look into platformCp final String resourceName = internalName + CLASS_RESOURCE_EXTENSION; @@ -123,19 +141,7 @@ public class InstrumentationClassFinder { if (is == null) { is = lookupClassAfterClasspath(internalName); } - - if (is == null) { - throw new ClassNotFoundException("Class not found: " + internalName); - } - - try { - final PseudoClass result = loadPseudoClass(is); - myLoaded.put(internalName, result); - return result; - } - finally { - is.close(); - } + return is; } public InputStream getResourceAsStream(String resourceName) throws IOException { diff --git a/plugins/IntelliLang/intellilang-jps-plugin/intellilang-jps-plugin.iml b/plugins/IntelliLang/intellilang-jps-plugin/intellilang-jps-plugin.iml new file mode 100644 index 000000000000..58257c150340 --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/intellilang-jps-plugin.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/META-INF/services/org.jetbrains.jps.incremental.BuilderService b/plugins/IntelliLang/intellilang-jps-plugin/src/META-INF/services/org.jetbrains.jps.incremental.BuilderService new file mode 100644 index 000000000000..41b8d9f78385 --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/META-INF/services/org.jetbrains.jps.incremental.BuilderService @@ -0,0 +1 @@ +org.jetbrains.jps.intellilang.instrumentation.PatternValidatorBuilderService \ No newline at end of file diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/META-INF/services/org.jetbrains.jps.intellilang.model.JpsIntelliLangExtensionService b/plugins/IntelliLang/intellilang-jps-plugin/src/META-INF/services/org.jetbrains.jps.intellilang.model.JpsIntelliLangExtensionService new file mode 100644 index 000000000000..c22fada5abbd --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/META-INF/services/org.jetbrains.jps.intellilang.model.JpsIntelliLangExtensionService @@ -0,0 +1 @@ +org.jetbrains.jps.intellilang.model.impl.JpsIntelliLangExtensionServiceImpl \ No newline at end of file diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/META-INF/services/org.jetbrains.jps.model.serialization.JpsModelSerializerExtension b/plugins/IntelliLang/intellilang-jps-plugin/src/META-INF/services/org.jetbrains.jps.model.serialization.JpsModelSerializerExtension new file mode 100644 index 000000000000..7dc93666d56a --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/META-INF/services/org.jetbrains.jps.model.serialization.JpsModelSerializerExtension @@ -0,0 +1 @@ +org.jetbrains.jps.intellilang.model.impl.JpsIntelliLangModelSerializerExtension \ No newline at end of file diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/InstrumentationAdapter.java b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/InstrumentationAdapter.java new file mode 100644 index 000000000000..738b091798d9 --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/InstrumentationAdapter.java @@ -0,0 +1,259 @@ +/* + * 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 org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.asm4.*; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +class InstrumentationAdapter extends MethodVisitor implements Opcodes { + @NonNls + private static final String RETURN_VALUE_NAME = "$returnvalue$"; + + private final Type[] myArgTypes; + private final Type myReturnType; + private final int myAccess; + private final String myMethodName; + + private final PatternInstrumenter myInstrumenter; + + private final List myParameterPatterns = new ArrayList(); + private PatternValue myMethodPattern; + + private Label myAssertLabel; + + public InstrumentationAdapter(PatternInstrumenter instrumenter, + MethodVisitor methodvisitor, + Type[] argTypes, + Type returnType, + int access, + String name) { + super(Opcodes.ASM4, methodvisitor); + myInstrumenter = instrumenter; + myArgTypes = argTypes; + myReturnType = returnType; + myAccess = access; + myMethodName = name; + } + + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + final AnnotationVisitor annotationvisitor = mv.visitParameterAnnotation(parameter, desc, visible); + + if (myArgTypes[parameter].getSort() == Type.OBJECT) { + final String annotationClassName = Type.getType(desc).getClassName(); + if (myInstrumenter.acceptAnnotation(annotationClassName)) { + @Nullable final String patternString = myInstrumenter.getAnnotationPattern(annotationClassName); + final String[] strings = annotationClassName.split("\\."); + final PatternValue patternValue = new PatternValue(parameter, strings[strings.length - 1], patternString); + myParameterPatterns.add(patternValue); + + // dig into the annotation and get the "value" element if pattern isn't present yet + return patternString == null ? new MyAnnotationVisitor(annotationvisitor, patternValue) : annotationvisitor; + } + } + return annotationvisitor; + } + + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + final AnnotationVisitor annotationvisitor = mv.visitAnnotation(desc, visible); + + if (myReturnType.getSort() == Type.OBJECT) { + final String annotationClassName = Type.getType(desc).getClassName(); + if (myInstrumenter.acceptAnnotation(annotationClassName)) { + @Nullable final String pattern = myInstrumenter.getAnnotationPattern(annotationClassName); + final String[] strings = annotationClassName.split("\\."); + myMethodPattern = new PatternValue(-1, strings[strings.length - 1], pattern); + + return pattern == null ? new MyAnnotationVisitor(annotationvisitor, myMethodPattern) : annotationvisitor; + } + } + return annotationvisitor; + } + + public void visitCode() { + for (PatternValue parameter : myParameterPatterns) { + int j; + if ((myAccess & Opcodes.ACC_STATIC) == 0) { + // special case: ctor of non-static inner classes (see IDEA-10889) + if ("".equals(myMethodName)) { + // ACC_INTERFACE is (ab-)used to tunnel the information about the non-static inner class + j = (myInstrumenter.myIsNonStaticInnerClass) ? 1 + myArgTypes[0].getSize() // skip first (synthetic) "Outer.this" parameter + : 1; + } + else { + j = 1; + } + } + else { + j = 0; + } + + for (int l = 0; l < parameter.index; l++) { + j += myArgTypes[l].getSize(); + } + + final Label checked = new Label(); + + addPatternTest(parameter.patternIndex, checked, j); + + addPatternAssertion(MessageFormat.format("Argument {0} for @{1} parameter of {2}.{3} does not match pattern {4}", parameter.index, + parameter.annotation, myInstrumenter.myClassName, myMethodName, parameter.pattern), false); + + mv.visitLabel(checked); + } + + if (myMethodPattern != null) { + myAssertLabel = new Label(); + } + } + + public void visitInsn(int opcode) { + if (opcode == Opcodes.ARETURN && myAssertLabel != null) { + mv.visitJumpInsn(Opcodes.GOTO, myAssertLabel); + } + else { + mv.visitInsn(opcode); + } + } + + public void visitMaxs(int maxStack, int maxLocals) { + if (myAssertLabel != null) { + + // next index for synthetic variable that holds return value + final int var = maxLocals + 1; + + mv.visitLabel(myAssertLabel); + + mv.visitVarInsn(Opcodes.ASTORE, var); + + final Label end = new Label(); + addPatternTest(myMethodPattern.patternIndex, end, var); + + addPatternAssertion(MessageFormat.format("Return value of method {0}.{1} annotated as @{2} does not match pattern {3}", + myInstrumenter.myClassName, myMethodName, myMethodPattern.annotation, + myMethodPattern.pattern), true); + + mv.visitLabel(end); + mv.visitLocalVariable(RETURN_VALUE_NAME, PatternInstrumenter.JAVA_LANG_STRING, null, myAssertLabel, end, var); + + mv.visitVarInsn(Opcodes.ALOAD, var); + mv.visitInsn(Opcodes.ARETURN); + } + + super.visitMaxs(maxStack, maxLocals); + } + + @SuppressWarnings({"HardCodedStringLiteral"}) + private void addPatternTest(int patternIndex, Label label, int varIndex) { + if (myInstrumenter.myInstrumentationType == InstrumentationType.ASSERT) { + mv.visitFieldInsn(Opcodes.GETSTATIC, myInstrumenter.myClassName, PatternInstrumenter.ASSERTIONS_DISABLED_NAME, "Z"); + mv.visitJumpInsn(Opcodes.IFNE, label); + } + + mv.visitVarInsn(Opcodes.ALOAD, varIndex); + mv.visitJumpInsn(Opcodes.IFNULL, label); + + mv.visitFieldInsn(GETSTATIC, myInstrumenter.myClassName, PatternInstrumenter.PATTERN_CACHE_NAME, + "[Ljava/util/regex/Pattern;"); + mv.visitIntInsn(BIPUSH, patternIndex); + mv.visitInsn(AALOAD); + mv.visitVarInsn(ALOAD, varIndex); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/util/regex/Pattern", "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/util/regex/Matcher", "matches", "()Z"); + + mv.visitJumpInsn(Opcodes.IFNE, label); + } + + // TODO: add actual value to assertion message + private void addPatternAssertion(String message, boolean isMethod) { + if (myInstrumenter.myInstrumentationType == InstrumentationType.ASSERT) { + addThrow("java/lang/AssertionError", "(Ljava/lang/Object;)V", message); + } + else if (myInstrumenter.myInstrumentationType == InstrumentationType.EXCEPTION) { + if (isMethod) { + addThrow("java/lang/IllegalStateException", "(Ljava/lang/String;)V", message); + } + else { + addThrow("java/lang/IllegalArgumentException", "(Ljava/lang/String;)V", message); + } + } + myInstrumenter.myInstrumented = true; + } + + private void addThrow(@NonNls String throwableClass, @NonNls String ctorSignature, String message) { + mv.visitTypeInsn(Opcodes.NEW, throwableClass); + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(message); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, throwableClass, "", ctorSignature); + mv.visitInsn(Opcodes.ATHROW); + } + + private static class MyAnnotationVisitor extends AnnotationVisitor { + private final AnnotationVisitor av; + private final PatternValue myPatternValue; + + public MyAnnotationVisitor(AnnotationVisitor annotationvisitor, PatternValue v) { + super(Opcodes.ASM4); + av = annotationvisitor; + myPatternValue = v; + } + + public void visit(@NonNls String name, Object value) { + av.visit(name, value); + if ("value".equals(name) && value instanceof String) { + myPatternValue.set((String)value); + } + } + + public void visitEnum(String name, String desc, String value) { + av.visitEnum(name, desc, value); + } + + public AnnotationVisitor visitAnnotation(String name, String desc) { + return av.visitAnnotation(name, desc); + } + + public AnnotationVisitor visitArray(String name) { + return av.visitArray(name); + } + + public void visitEnd() { + av.visitEnd(); + } + } + + class PatternValue { + final int index; + final String annotation; + String pattern; + int patternIndex; + + PatternValue(int index, String annotation, String pattern) { + this.index = index; + this.annotation = annotation; + if (pattern != null) set(pattern); + } + + void set(String s) { + assert pattern == null; + patternIndex = myInstrumenter.addPattern(pattern = s); + } + } +} diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/InstrumentationType.java b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/InstrumentationType.java new file mode 100644 index 000000000000..c9c3966a9e92 --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/InstrumentationType.java @@ -0,0 +1,21 @@ +/* + * 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; + +public enum InstrumentationType { + NONE, ASSERT, EXCEPTION +} + diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/PatternInstrumenter.java b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/PatternInstrumenter.java new file mode 100644 index 000000000000..6d1bd2d249e3 --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/PatternInstrumenter.java @@ -0,0 +1,276 @@ +/* + * 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.asm4.*; +import org.jetbrains.jps.intellilang.model.InstrumentationException; + +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 myPatterns = new LinkedHashSet(); + + private final String myPatternAnnotationClassName; + final InstrumentationType myInstrumentationType; + private final InstrumentationClassFinder myClassFinder; + private final Map myAnnotationNameToPatternMap = new HashMap(); // can contain null values! + private final Set myProcessedAnnotations = new HashSet(); // checked annotation classes + + String myClassName; + boolean myInstrumented; + boolean myIsNonStaticInnerClass; + + public PatternInstrumenter(@NotNull String patternAnnotationClassName, ClassVisitor classvisitor, + InstrumentationType instrumentation, + InstrumentationClassFinder classFinder) { + super(Opcodes.ASM4, 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; + } + + 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, "", "()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;"); + 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"); + 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("")) { + myHasStaticInitializer = true; + + return new MethodVisitor(Opcodes.ASM4, 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 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) { + 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 patternString = new Ref(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.ASM4) { + @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.ASM4) { + 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 + } + } +} diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/PatternValidatorBuilder.java b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/PatternValidatorBuilder.java new file mode 100644 index 000000000000..5403315001c5 --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/PatternValidatorBuilder.java @@ -0,0 +1,82 @@ +/* + * 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 org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.asm4.ClassReader; +import org.jetbrains.asm4.ClassWriter; +import org.jetbrains.jps.ModuleChunk; +import org.jetbrains.jps.incremental.CompileContext; +import org.jetbrains.jps.incremental.CompiledClass; +import org.jetbrains.jps.incremental.instrumentation.BaseInstrumentingBuilder; +import org.jetbrains.jps.incremental.messages.BuildMessage; +import org.jetbrains.jps.incremental.messages.CompilerMessage; +import org.jetbrains.jps.intellilang.model.InstrumentationException; +import org.jetbrains.jps.intellilang.model.JpsIntelliLangConfiguration; +import org.jetbrains.jps.intellilang.model.JpsIntelliLangExtensionService; +import org.jetbrains.jps.javac.BinaryContent; + +/** + * @author Eugene Zhuravlev + * Date: 11/25/12 + */ +public class PatternValidatorBuilder extends BaseInstrumentingBuilder{ + public PatternValidatorBuilder() { + } + + @NotNull + @Override + public String getPresentableName() { + return "IntelliLang Pattern Validator"; + } + + @Override + protected boolean isEnabled(CompileContext context, ModuleChunk chunk) { + final JpsIntelliLangConfiguration config = JpsIntelliLangExtensionService.getInstance().getConfiguration(context.getProjectDescriptor().getModel().getGlobal()); + return config.getInstrumentationType() != InstrumentationType.NONE; + } + + @Override + protected boolean canInstrument(CompiledClass compiledClass, int classFileVersion) { + return true; + } + + @Nullable + @Override + protected BinaryContent instrument(CompileContext context, CompiledClass compiled, ClassReader reader, ClassWriter writer, InstrumentationClassFinder finder) { + final JpsIntelliLangConfiguration config = + JpsIntelliLangExtensionService.getInstance().getConfiguration(context.getProjectDescriptor().getModel().getGlobal()); + final PatternInstrumenter instrumenter = + new PatternInstrumenter(config.getPatternAnnotationClass(), writer, config.getInstrumentationType(), finder); + try { + reader.accept(instrumenter, 0); + if (instrumenter.instrumented()) { + return new BinaryContent(writer.toByteArray()); + } + } + catch (InstrumentationException e) { + context.processMessage(new CompilerMessage(getPresentableName(), BuildMessage.Kind.ERROR, e.getMessage())); + } + return null; + } + + @Override + protected String getProgressMessage() { + return "Adding pattern assertions..."; + } +} diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/PatternValidatorBuilderService.java b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/PatternValidatorBuilderService.java new file mode 100644 index 000000000000..39ba6dee9f69 --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/instrumentation/PatternValidatorBuilderService.java @@ -0,0 +1,35 @@ +/* + * 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 org.jetbrains.annotations.NotNull; +import org.jetbrains.jps.incremental.BuilderService; +import org.jetbrains.jps.incremental.ModuleLevelBuilder; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Eugene Zhuravlev + * Date: 11/28/12 + */ +public class PatternValidatorBuilderService extends BuilderService{ + @NotNull + @Override + public List createModuleLevelBuilders() { + return Arrays.asList(new PatternValidatorBuilder()); + } +} diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/InstrumentationException.java b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/InstrumentationException.java new file mode 100644 index 000000000000..f36f812d69bd --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/InstrumentationException.java @@ -0,0 +1,26 @@ +/* + * 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.model; + +public class InstrumentationException extends RuntimeException { + public InstrumentationException(String s) { + super(s); + } + + public InstrumentationException(String s, Exception e) { + super(s, e); + } +} diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/JpsIntelliLangConfiguration.java b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/JpsIntelliLangConfiguration.java new file mode 100644 index 000000000000..02394570a07d --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/JpsIntelliLangConfiguration.java @@ -0,0 +1,14 @@ +package org.jetbrains.jps.intellilang.model; + +import org.jetbrains.jps.intellilang.instrumentation.InstrumentationType; +import org.jetbrains.jps.model.JpsElement; + +/** + * @author Eugene Zhuravlev + * Date: 11/29/12 + */ +public interface JpsIntelliLangConfiguration extends JpsElement { + String getPatternAnnotationClass(); + + InstrumentationType getInstrumentationType(); +} diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/JpsIntelliLangExtensionService.java b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/JpsIntelliLangExtensionService.java new file mode 100644 index 000000000000..a59a05405772 --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/JpsIntelliLangExtensionService.java @@ -0,0 +1,20 @@ +package org.jetbrains.jps.intellilang.model; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.jps.model.JpsGlobal; +import org.jetbrains.jps.service.JpsServiceManager; + +/** + * @author Eugene Zhuravlev + * Date: 11/29/12 + */ +public abstract class JpsIntelliLangExtensionService { + public static JpsIntelliLangExtensionService getInstance() { + return JpsServiceManager.getInstance().getService(JpsIntelliLangExtensionService.class); + } + + @NotNull + public abstract JpsIntelliLangConfiguration getConfiguration(@NotNull JpsGlobal project); + + public abstract void setConfiguration(@NotNull JpsGlobal project, @NotNull JpsIntelliLangConfiguration extension); +} diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/impl/JpsIntelliLangConfigurationImpl.java b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/impl/JpsIntelliLangConfigurationImpl.java new file mode 100644 index 000000000000..3c74702d29ff --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/impl/JpsIntelliLangConfigurationImpl.java @@ -0,0 +1,50 @@ +package org.jetbrains.jps.intellilang.model.impl; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.jps.intellilang.instrumentation.InstrumentationType; +import org.jetbrains.jps.intellilang.model.JpsIntelliLangConfiguration; +import org.jetbrains.jps.model.JpsElementChildRole; +import org.jetbrains.jps.model.ex.JpsElementBase; +import org.jetbrains.jps.model.ex.JpsElementChildRoleBase; + +/** + * @author Eugene Zhuravlev + * Date: 11/29/12 + */ +public class JpsIntelliLangConfigurationImpl extends JpsElementBase implements + JpsIntelliLangConfiguration { + public static final JpsElementChildRole ROLE = JpsElementChildRoleBase.create("IntelliLang"); + + private String myPatternAnnotationClassName = "org.intellij.lang.annotations.Pattern"; + private InstrumentationType myInstrumentationType = InstrumentationType.ASSERT; + + @NotNull + @Override + public JpsIntelliLangConfigurationImpl createCopy() { + return new JpsIntelliLangConfigurationImpl(); + } + + @Override + public void applyChanges(@NotNull JpsIntelliLangConfigurationImpl modified) { + myPatternAnnotationClassName = modified.myPatternAnnotationClassName; + myInstrumentationType = modified.myInstrumentationType; + } + + @Override + public String getPatternAnnotationClass() { + return myPatternAnnotationClassName; + } + + @Override + public InstrumentationType getInstrumentationType() { + return myInstrumentationType; + } + + public void setPatternAnnotationClassName(String patternAnnotationClassName) { + myPatternAnnotationClassName = patternAnnotationClassName; + } + + public void setInstrumentationType(InstrumentationType instrumentationType) { + myInstrumentationType = instrumentationType; + } +} diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/impl/JpsIntelliLangConfigurationSerializer.java b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/impl/JpsIntelliLangConfigurationSerializer.java new file mode 100644 index 000000000000..385d0937e9ff --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/impl/JpsIntelliLangConfigurationSerializer.java @@ -0,0 +1,56 @@ +package org.jetbrains.jps.intellilang.model.impl; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.JDOMExternalizerUtil; +import org.jdom.Element; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.jps.intellilang.instrumentation.InstrumentationType; +import org.jetbrains.jps.intellilang.model.JpsIntelliLangExtensionService; +import org.jetbrains.jps.model.JpsGlobal; +import org.jetbrains.jps.model.serialization.JpsGlobalExtensionSerializer; + +/** + * @author Eugene Zhuravlev + * Date: 11/29/12 + */ +public class JpsIntelliLangConfigurationSerializer extends JpsGlobalExtensionSerializer { + private static final Logger LOG = Logger.getInstance("#org.jetbrains.jps.intellilang.model.impl.JpsIntelliLangConfigurationSerializer"); + private static final String INSTRUMENTATION_TYPE_NAME = "INSTRUMENTATION"; + private static final String PATTERN_ANNOTATION_NAME = "PATTERN_ANNOTATION"; + + public JpsIntelliLangConfigurationSerializer() { + super("IntelliLang.xml", "LanguageInjectionConfiguration"); + } + + @Override + public void loadExtension(@NotNull JpsGlobal global, @NotNull Element componentTag) { + final JpsIntelliLangConfigurationImpl configuration = new JpsIntelliLangConfigurationImpl(); + + final String annotationName = JDOMExternalizerUtil.readField(componentTag, PATTERN_ANNOTATION_NAME); + if (annotationName != null) { + configuration.setPatternAnnotationClassName(annotationName); + } + + final String instrumentationType = JDOMExternalizerUtil.readField(componentTag, INSTRUMENTATION_TYPE_NAME); + if (instrumentationType != null) { + try { + final InstrumentationType type = InstrumentationType.valueOf(instrumentationType); + configuration.setInstrumentationType(type); + } + catch (IllegalArgumentException ignored) { + LOG.info(ignored); + } + } + + JpsIntelliLangExtensionService.getInstance().setConfiguration(global, configuration); + } + + @Override + public void loadExtensionWithDefaultSettings(@NotNull JpsGlobal global) { + JpsIntelliLangExtensionService.getInstance().setConfiguration(global, new JpsIntelliLangConfigurationImpl()); + } + + @Override + public void saveExtension(@NotNull JpsGlobal global, @NotNull Element componentTag) { + } +} diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/impl/JpsIntelliLangExtensionServiceImpl.java b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/impl/JpsIntelliLangExtensionServiceImpl.java new file mode 100644 index 000000000000..1fa016afeb12 --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/impl/JpsIntelliLangExtensionServiceImpl.java @@ -0,0 +1,23 @@ +package org.jetbrains.jps.intellilang.model.impl; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.jps.intellilang.model.JpsIntelliLangConfiguration; +import org.jetbrains.jps.intellilang.model.JpsIntelliLangExtensionService; +import org.jetbrains.jps.model.JpsGlobal; + +/** + * @author Eugene Zhuravlev + * Date: 11/29/12 + */ +public class JpsIntelliLangExtensionServiceImpl extends JpsIntelliLangExtensionService { + @NotNull + @Override + public JpsIntelliLangConfiguration getConfiguration(@NotNull JpsGlobal global) { + return global.getContainer().getChild(JpsIntelliLangConfigurationImpl.ROLE); + } + + @Override + public void setConfiguration(@NotNull JpsGlobal global, @NotNull JpsIntelliLangConfiguration config) { + global.getContainer().setChild(JpsIntelliLangConfigurationImpl.ROLE, config); + } +} diff --git a/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/impl/JpsIntelliLangModelSerializerExtension.java b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/impl/JpsIntelliLangModelSerializerExtension.java new file mode 100644 index 000000000000..609939c758a7 --- /dev/null +++ b/plugins/IntelliLang/intellilang-jps-plugin/src/org/jetbrains/jps/intellilang/model/impl/JpsIntelliLangModelSerializerExtension.java @@ -0,0 +1,20 @@ +package org.jetbrains.jps.intellilang.model.impl; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.jps.model.serialization.JpsGlobalExtensionSerializer; +import org.jetbrains.jps.model.serialization.JpsModelSerializerExtension; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Eugene Zhuravlev + * Date: 11/29/12 + */ +public class JpsIntelliLangModelSerializerExtension extends JpsModelSerializerExtension{ + @NotNull + @Override + public List getGlobalExtensionSerializers() { + return Arrays.asList(new JpsIntelliLangConfigurationSerializer()); + } +} diff --git a/plugins/IntelliLang/src/META-INF/plugin.xml b/plugins/IntelliLang/src/META-INF/plugin.xml index 96c446486839..01845b44d776 100644 --- a/plugins/IntelliLang/src/META-INF/plugin.xml +++ b/plugins/IntelliLang/src/META-INF/plugin.xml @@ -43,5 +43,7 @@ serviceImplementation="org.intellij.plugins.intelliLang.Configuration$App"/> + +