diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/ClassBuilder.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/ClassBuilder.java new file mode 100644 index 0000000000..36771913e7 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/ClassBuilder.java @@ -0,0 +1,401 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet; + +import static net.fabricmc.mappingpoet.FieldBuilder.parseAnnotation; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.TypeSpec; +import com.squareup.javapoet.TypeVariableName; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.TypeReference; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InnerClassNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import net.fabricmc.mappingpoet.signature.AnnotationAwareDescriptors; +import net.fabricmc.mappingpoet.signature.AnnotationAwareSignatures; +import net.fabricmc.mappingpoet.signature.ClassSignature; +import net.fabricmc.mappingpoet.signature.TypeAnnotationMapping; +import net.fabricmc.mappingpoet.signature.TypeAnnotationStorage; + +public class ClassBuilder { + static final Handle OBJ_MTH_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + "java/lang/runtime/ObjectMethods", + "bootstrap", + "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;", + false); + + private final MappingsStore mappings; + private final ClassNode classNode; + + private final TypeSpec.Builder builder; + private final List innerClasses = new ArrayList<>(); + private final Environment environment; + + private final ClassSignature signature; // not really signature + private final TypeAnnotationMapping typeAnnotations; + private boolean annotationClass; + private boolean enumClass; + private boolean recordClass; + private boolean instanceInner = false; + // only nonnull if any class in the inner class chain creates a generic decl + // omits L and ; + private String receiverSignature; + + public ClassBuilder(MappingsStore mappings, ClassNode classNode, Environment environment) { + this.mappings = mappings; + this.classNode = classNode; + this.environment = environment; + this.typeAnnotations = setupAnnotations(); + this.signature = setupSignature(); + this.builder = setupBuilder(); + + addInterfaces(); + addAnnotations(); + addJavaDoc(); + } + + public static ClassName parseInternalName(String internalName) { + int classNameSeparator = -1; + int index = 0; + int nameStart = index; + ClassName currentClassName = null; + + char ch; + do { + ch = index == internalName.length() ? ';' : internalName.charAt(index); + + if (ch == '$' || ch == ';') { + // collect class name + if (currentClassName == null) { + String packageName = nameStart < classNameSeparator ? internalName.substring(nameStart, classNameSeparator).replace('/', '.') : ""; + String simpleName = internalName.substring(classNameSeparator + 1, index); + currentClassName = ClassName.get(packageName, simpleName); + } else { + String simpleName = internalName.substring(classNameSeparator + 1, index); + currentClassName = currentClassName.nestedClass(simpleName); + } + } + + if (ch == '/' || ch == '$') { + // Start of simple name + classNameSeparator = index; + } + + index++; + } while (ch != ';'); + + if (currentClassName == null) { + throw new IllegalArgumentException(String.format("Invalid internal name \"%s\"", internalName)); + } + + return currentClassName; + } + + private TypeAnnotationMapping setupAnnotations() { + return TypeAnnotationStorage.builder() + .add(classNode.invisibleTypeAnnotations) + .add(classNode.visibleTypeAnnotations) + .build(); + } + + public void addMembers() { + addMethods(); + addFields(); + } + + private ClassSignature setupSignature() { + if (classNode.signature == null) { + return AnnotationAwareDescriptors.parse(classNode.superName, classNode.interfaces, typeAnnotations, environment); + } else { + return AnnotationAwareSignatures.parseClassSignature(classNode.signature, typeAnnotations, environment); + } + } + + private TypeSpec.Builder setupBuilder() { + TypeSpec.Builder builder; + ClassName name = parseInternalName(classNode.name); // no type anno here + + if (Modifier.isInterface(classNode.access)) { + if (classNode.interfaces.size() == 1 && classNode.interfaces.get(0).equals("java/lang/annotation/Annotation")) { + builder = TypeSpec.annotationBuilder(name); + this.annotationClass = true; + } else { + builder = TypeSpec.interfaceBuilder(name); + } + } else if (classNode.superName.equals("java/lang/Enum")) { + enumClass = true; + builder = TypeSpec.enumBuilder(name); + } else if (classNode.superName.equals("java/lang/Record")) { + recordClass = true; + builder = TypeSpec.recordBuilder(name); + } else { + builder = TypeSpec.classBuilder(name) + .superclass(signature.superclass()); + } + + if (!signature.generics().isEmpty()) { + builder.addTypeVariables(signature.generics()); + StringBuilder sb = new StringBuilder(); + sb.append(classNode.name); + sb.append("<"); + for (TypeVariableName each : signature.generics()) { + sb.append("T").append(each.name).append(";"); + } + sb.append(">"); + receiverSignature = sb.toString(); + } + + return builder + .addModifiers(new ModifierBuilder(classNode.access) + .checkUnseal(classNode, environment) + .getModifiers(ModifierBuilder.getType(enumClass, recordClass))); + } + + private void addInterfaces() { + if (annotationClass) { + return; + } + + if (signature != null) { + builder.addSuperinterfaces(signature.superinterfaces()); + return; + } + + if (classNode.interfaces.isEmpty()) return; + + for (String iFace : classNode.interfaces) { + builder.addSuperinterface(parseInternalName(iFace)); + } + } + + private void addAnnotations() { + // type anno already done through class sig + addDirectAnnotations(classNode.invisibleAnnotations); + addDirectAnnotations(classNode.visibleAnnotations); + } + + private void addDirectAnnotations(List regularAnnotations) { + if (regularAnnotations == null) { + return; + } + + for (AnnotationNode annotation : regularAnnotations) { + builder.addAnnotation(parseAnnotation(annotation)); + } + } + + private void addMethods() { + if (classNode.methods == null) return; + + methodsLoop: for (MethodNode method : classNode.methods) { + if ((method.access & Opcodes.ACC_SYNTHETIC) != 0 || (method.access & Opcodes.ACC_MANDATED) != 0) { + continue; + } + + if (method.name.equals("")) { + continue; + } + + int formalParamStartIndex = 0; + + if (enumClass) { + // Skip enum sugar methods + if (method.name.equals("values") && method.desc.equals("()[L" + classNode.name + ";")) { + continue; + } + + if (method.name.equals("valueOf") && method.desc.equals("(Ljava/lang/String;)L" + classNode.name + ";")) { + continue; + } + + if (method.name.equals("")) { + formalParamStartIndex = 2; // 0 String 1 int + } + } + + if (recordClass) { + // skip record sugars + if (method.name.equals("equals") && method.desc.equals("(Ljava/lang/Object;)Z") + || method.name.equals("toString") && method.desc.equals("()Ljava/lang/String;") + || method.name.equals("hashCode") && method.desc.equals("()I")) { + for (AbstractInsnNode insn : method.instructions) { + if (insn instanceof InvokeDynamicInsnNode indy + && indy.bsm.equals(OBJ_MTH_BOOTSTRAP) + && indy.name.equals(method.name)) + continue methodsLoop; + } + } + + // todo test component getters + } + + if (instanceInner) { + if (method.name.equals("")) { + formalParamStartIndex = 1; // 0 this$0 + } + } + + builder.addMethod(new MethodBuilder(mappings, classNode, method, environment, receiverSignature, formalParamStartIndex).build()); + } + } + + private void addFields() { + if (classNode.fields == null) return; + + for (FieldNode field : classNode.fields) { + if (recordClass && !Modifier.isStatic(field.access)) { + // proguard elevates record field access for direct record field gets + if (!Modifier.isFinal(field.access) || Modifier.isProtected(field.access) || Modifier.isPublic(field.access)) { + System.out.println("abnormal instance field " + field.name + " in record " + getClassName() + ", skipping"); + } else { + var fieldBuilder = new FieldBuilder(mappings, classNode, field, environment); + var paramBuilder = ParameterSpec.builder(fieldBuilder.calculateType(), field.name); + fieldBuilder.addJavaDoc(paramBuilder); + fieldBuilder.addDirectAnnotations(paramBuilder); + builder.addRecordComponent(paramBuilder.build()); + } + + continue; + } + + if ((field.access & Opcodes.ACC_SYNTHETIC) != 0 || (field.access & Opcodes.ACC_MANDATED) != 0) { + continue; // hide synthetic stuff + } + + if ((field.access & Opcodes.ACC_ENUM) == 0) { + builder.addField(new FieldBuilder(mappings, classNode, field, environment).build()); + } else { + TypeSpec.Builder enumBuilder = TypeSpec.anonymousClassBuilder(""); + // jd + FieldBuilder.addFieldJavaDoc(enumBuilder, mappings, classNode, field); + + // annotations + addDirectAnnotations(enumBuilder, field.invisibleAnnotations); + addDirectAnnotations(enumBuilder, field.visibleAnnotations); + List annotations = TypeAnnotationStorage.builder() + .add(field.invisibleTypeAnnotations) + .add(field.visibleTypeAnnotations) + .build().getBank(TypeReference.newTypeReference(TypeReference.FIELD)) + .getCurrentAnnotations(); + + if (!annotations.isEmpty()) { + enumBuilder.addAnnotations(annotations); // no custom paths for annotations rip + } + + builder.addEnumConstant(field.name, enumBuilder.build()); + } + } + } + + private void addDirectAnnotations(TypeSpec.Builder builder, List regularAnnotations) { + if (regularAnnotations == null) { + return; + } + + for (AnnotationNode annotation : regularAnnotations) { + builder.addAnnotation(parseAnnotation(annotation)); + } + } + + private void addJavaDoc() { + mappings.addClassDoc(builder::addJavadoc, classNode.name); + } + + public void addInnerClass(ClassBuilder classBuilder) { + InnerClassNode innerClassNode = null; + + if (classNode.innerClasses != null) { + for (InnerClassNode node : classNode.innerClasses) { + if (node.name.equals(classBuilder.classNode.name)) { + innerClassNode = node; + break; + } + } + } + + if (innerClassNode == null) { + // fallback + classBuilder.builder.addModifiers(javax.lang.model.element.Modifier.PUBLIC); + classBuilder.builder.addModifiers(javax.lang.model.element.Modifier.STATIC); + } else { + if (innerClassNode.outerName == null) { + // skip local classes and records, which have null outerName + return; + } + + if (classBuilder.classNode.outerMethod != null) { + // local class per EnclosingMethod attribute + return; + } + + classBuilder.builder.modifiers.remove(javax.lang.model.element.Modifier.PUBLIC); // this modifier may come from class access + classBuilder.builder.addModifiers(new ModifierBuilder(innerClassNode.access) + .checkUnseal(classBuilder.classNode, environment) + .getModifiers(ModifierBuilder.getType(classBuilder.enumClass, classBuilder.recordClass))); + + if (!Modifier.isStatic(innerClassNode.access)) { + classBuilder.instanceInner = true; + } + // consider emit warning if this.instanceInner is true when classBuilder.instanceInner is false + + if (this.receiverSignature != null && classBuilder.instanceInner) { + StringBuilder sb = new StringBuilder(); + sb.append(this.receiverSignature).append("."); // like O. for O + sb.append(innerClassNode.innerName); // append simple name + + List innerClassGenerics = classBuilder.signature.generics(); + + if (!innerClassGenerics.isEmpty()) { + sb.append("<"); + for (TypeVariableName each : innerClassGenerics) { + sb.append("T").append(each.name).append(";"); + } + sb.append(">"); + } + + classBuilder.receiverSignature = sb.toString(); + } + } + + innerClasses.add(classBuilder); + } + + public String getClassName() { + return classNode.name; + } + + public TypeSpec build() { + for (ClassBuilder innerCb : innerClasses) { + builder.addType(innerCb.build()); + } + + return builder.build(); + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/Environment.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/Environment.java new file mode 100644 index 0000000000..5355634b2f --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/Environment.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet; + +import com.squareup.javapoet.ClassName; +import net.fabricmc.mappingpoet.signature.ClassStaticContext; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * Represents an overall runtime environment, knows all inner class, + * super class, etc. information. + */ +public record Environment( + Map> superTypes, + Set sealedClasses, + // declaring classes keep track of namable inner classes + // and local/anon classes in whole codebase + Map declaringClasses +) implements ClassStaticContext { + public record NestedClassInfo(String declaringClass, boolean instanceInner, String simpleName) { + // two strings are nullable + } + + public record ClassNamePointer(String simple, String outerClass) { + public ClassName toClassName(ClassName outerClassName) { + if (simple == null) + return null; + + return outerClassName.nestedClass(simple); + } + } + + @Override + public boolean isInstanceInner(String internalName) { + var info = declaringClasses.get(internalName); + return info != null && info.declaringClass != null && info.instanceInner; + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/FieldBuilder.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/FieldBuilder.java new file mode 100644 index 0000000000..1bf98c93a6 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/FieldBuilder.java @@ -0,0 +1,514 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet; + +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import net.fabricmc.mappingpoet.signature.AnnotationAwareDescriptors; +import net.fabricmc.mappingpoet.signature.AnnotationAwareSignatures; +import net.fabricmc.mappingpoet.signature.ClassStaticContext; +import net.fabricmc.mappingpoet.signature.TypeAnnotationBank; +import net.fabricmc.mappingpoet.signature.TypeAnnotationMapping; +import net.fabricmc.mappingpoet.signature.TypeAnnotationStorage; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.TypePath; +import org.objectweb.asm.TypeReference; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; + +import java.util.AbstractMap; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class FieldBuilder { + private final MappingsStore mappings; + private final ClassNode classNode; + private final FieldNode fieldNode; + private final FieldSpec.Builder builder; // todo extract interface to build both record param/field easily + private final TypeAnnotationMapping annotations; + private final ClassStaticContext context; + + public FieldBuilder(MappingsStore mappings, ClassNode classNode, FieldNode fieldNode, ClassStaticContext context) { + this.mappings = mappings; + this.classNode = classNode; + this.fieldNode = fieldNode; + this.context = context; + this.annotations = TypeAnnotationStorage.builder() + .add(fieldNode.invisibleTypeAnnotations) + .add(fieldNode.visibleTypeAnnotations) + .build(); + this.builder = createBuilder(); + addDirectAnnotations(); + addJavaDoc(); + } + + static void addFieldJavaDoc(TypeSpec.Builder enumBuilder, MappingsStore mappings, ClassNode classNode, FieldNode fieldNode) { + mappings.addFieldDoc(enumBuilder::addJavadoc, classNode.name, fieldNode.name, fieldNode.desc); + } + + public static AnnotationSpec parseAnnotation(AnnotationNode annotation) { + ClassName annoClassName = (ClassName) typeFromDesc(annotation.desc); + AnnotationSpec.Builder builder = AnnotationSpec.builder(annoClassName); + List values = annotation.values; + if (values != null) { + Iterator itr = values.iterator(); + while (itr.hasNext()) { + String key = (String) itr.next(); + Object value = itr.next(); + + builder.addMember(key, codeFromAnnoValue(value)); + } + } + + return builder.build(); + } + + public static CodeBlock codeFromAnnoValue(Object value) { + // BCDFIJSZ; String; String[] (for enum); asm type; anno node; list of any prev stuff (cannot nest) + if (value instanceof List) { + return ((List) value).stream().map(FieldBuilder::codeFromAnnoValue).collect(CodeBlock.joining(",", "{", "}")); + } + if (value instanceof Character || value instanceof Number || value instanceof Boolean) { + return CodeBlock.builder().add("$L", value).build(); + } + if (value instanceof String) { + return CodeBlock.builder().add("$S", value).build(); + } + if (value instanceof String[]) { + String[] arr = (String[]) value; + ClassName enumClassName = (ClassName) typeFromDesc(arr[0]); + String valueName = arr[1]; + return CodeBlock.builder().add("$T.$L", enumClassName, valueName).build(); + } + if (value instanceof Type) { + return CodeBlock.builder().add("$T.class", typeFromDesc(((Type) value).getDescriptor())).build(); + } + if (value instanceof AnnotationNode) { + return CodeBlock.builder().add(parseAnnotation((AnnotationNode) value).toString()).build(); + } + throw new IllegalArgumentException(String.format("Don't know how to convert \"%s\" into annotation value", value)); + } + + public static TypeName typeFromDesc(final String desc) { + return parseType(desc, 0).getValue(); + } + + public static Map.Entry parseType(final String desc, final int start) { + int index = start; + int arrayLevel = 0; + while (desc.charAt(index) == '[') { + arrayLevel++; + index++; + } + + TypeName current; + switch (desc.charAt(index)) { + case 'B': { + current = TypeName.BYTE; + index++; + break; + } + case 'C': { + current = TypeName.CHAR; + index++; + break; + } + case 'D': { + current = TypeName.DOUBLE; + index++; + break; + } + case 'F': { + current = TypeName.FLOAT; + index++; + break; + } + case 'I': { + current = TypeName.INT; + index++; + break; + } + case 'J': { + current = TypeName.LONG; + index++; + break; + } + case 'S': { + current = TypeName.SHORT; + index++; + break; + } + case 'Z': { + current = TypeName.BOOLEAN; + index++; + break; + } + case 'V': { + current = TypeName.VOID; + index++; + break; + } + case 'L': { + int classNameSeparator = index; + index++; + int nameStart = index; + ClassName currentClassName = null; + + char ch; + do { + ch = desc.charAt(index); + + if (ch == '$' || ch == ';') { + // collect class name + if (currentClassName == null) { + String packageName = nameStart < classNameSeparator ? desc.substring(nameStart, classNameSeparator).replace('/', '.') : ""; + String simpleName = desc.substring(classNameSeparator + 1, index); + currentClassName = ClassName.get(packageName, simpleName); + } else { + String simpleName = desc.substring(classNameSeparator + 1, index); + currentClassName = currentClassName.nestedClass(simpleName); + } + } + + if (ch == '/' || ch == '$') { + // Start of simple name + classNameSeparator = index; + } + + index++; + } while (ch != ';'); + + if (currentClassName == null) { + throw invalidDesc(desc, index); + } + + current = currentClassName; + break; + } + default: + throw invalidDesc(desc, index); + } + + for (int i = 0; i < arrayLevel; i++) { + current = ArrayTypeName.of(current); + } + + return new AbstractMap.SimpleImmutableEntry<>(index, current); + } + + public static Map.Entry parseAnnotatedType(final String desc, final int start, TypeAnnotationBank annotations, ClassStaticContext context) { + int index = start; + Deque> arrayAnnos = new ArrayDeque<>(); + while (desc.charAt(index) == '[') { + arrayAnnos.push(annotations.getCurrentAnnotations()); + annotations = annotations.advance(TypePath.ARRAY_ELEMENT, 0); + index++; + } + + TypeName current; + switch (desc.charAt(index)) { + case 'B': { + current = TypeName.BYTE; + index++; + break; + } + case 'C': { + current = TypeName.CHAR; + index++; + break; + } + case 'D': { + current = TypeName.DOUBLE; + index++; + break; + } + case 'F': { + current = TypeName.FLOAT; + index++; + break; + } + case 'I': { + current = TypeName.INT; + index++; + break; + } + case 'J': { + current = TypeName.LONG; + index++; + break; + } + case 'S': { + current = TypeName.SHORT; + index++; + break; + } + case 'Z': { + current = TypeName.BOOLEAN; + index++; + break; + } + case 'V': { + current = TypeName.VOID; + index++; + break; + } + case 'L': { + int classNameSeparator = index; + index++; + int nameStart = index; + ClassName currentClassName = null; + boolean instanceInner = false; + + char ch; + do { + ch = desc.charAt(index); + + if (ch == '$' || ch == ';') { + // collect class name + if (currentClassName == null) { + String packageName = nameStart < classNameSeparator ? desc.substring(nameStart, classNameSeparator).replace('/', '.') : ""; + String simpleName = desc.substring(classNameSeparator + 1, index); + currentClassName = ClassName.get(packageName, simpleName); + } else { + String simpleName = desc.substring(classNameSeparator + 1, index); + + if (!instanceInner && context.isInstanceInner(desc.substring(nameStart, index))) { + instanceInner = true; + } + + currentClassName = currentClassName.nestedClass(simpleName); + + if (instanceInner) { + currentClassName = AnnotationAwareDescriptors.annotate(currentClassName, annotations); + annotations = annotations.advance(TypePath.INNER_TYPE, 0); + } + } + } + + if (ch == '/' || ch == '$') { + // Start of simple name + classNameSeparator = index; + } + + index++; + } while (ch != ';'); + + if (currentClassName == null) { + throw invalidDesc(desc, index); + } + + current = currentClassName; + break; + } + default: + throw invalidDesc(desc, index); + } + + while (!arrayAnnos.isEmpty()) { + current = ArrayTypeName.of(current); + List currentAnnos = arrayAnnos.pop(); + if (!currentAnnos.isEmpty()) { + current = current.annotated(currentAnnos); + } + } + + return new AbstractMap.SimpleImmutableEntry<>(index, current); + } + + private static IllegalArgumentException invalidDesc(String desc, int index) { + return new IllegalArgumentException(String.format("Invalid descriptor at index %d for \"%s\"", index, desc)); + } + + @Deprecated // use typeFromDesc, non-recursive + public static TypeName getFieldType(String desc) { + switch (desc) { + case "B": + return TypeName.BYTE; + case "C": + return TypeName.CHAR; + case "S": + return TypeName.SHORT; + case "Z": + return TypeName.BOOLEAN; + case "I": + return TypeName.INT; + case "J": + return TypeName.LONG; + case "F": + return TypeName.FLOAT; + case "D": + return TypeName.DOUBLE; + case "V": + return TypeName.VOID; + } + if (desc.startsWith("[")) { + return ArrayTypeName.of(getFieldType(desc.substring(1))); + } + if (desc.startsWith("L")) { + return ClassBuilder.parseInternalName(desc.substring(1).substring(0, desc.length() - 2)); + } + throw new UnsupportedOperationException("Unknown field type" + desc); + } + + private FieldSpec.Builder createBuilder() { + FieldSpec.Builder ret = FieldSpec.builder(calculateType(), fieldNode.name) + .addModifiers(new ModifierBuilder(fieldNode.access).getModifiers(ModifierBuilder.Type.FIELD)); + + if ((fieldNode.access & Opcodes.ACC_FINAL) != 0) { + ret.initializer(makeInitializer(fieldNode.desc)); // so jd doesn't complain about type mismatch + } + + return ret; + } + + private CodeBlock makeInitializer(String desc) { + // fake initializers exclude fields from constant values + switch (desc.charAt(0)) { + case 'B': + if (fieldNode.value instanceof Integer) { + return CodeBlock.builder().add("(byte) $L", fieldNode.value).build(); + } + // fake initializer falls through + case 'C': + if (fieldNode.value instanceof Integer) { + int value = (int) fieldNode.value; + char c = (char) value; + return printChar(CodeBlock.builder(), c, value).build(); + } + // fake initializer falls through + case 'D': + if (fieldNode.value instanceof Double) { + return CodeBlock.builder().add("$LD", fieldNode.value).build(); + } + // fake initializer falls through + case 'I': + if (fieldNode.value instanceof Integer) { + return CodeBlock.builder().add("$L", fieldNode.value).build(); + } + // fake initializer falls through + case 'J': + if (fieldNode.value instanceof Long) { + return CodeBlock.builder().add("$LL", fieldNode.value).build(); + } + // fake initializer falls through + case 'S': + if (fieldNode.value instanceof Integer) { + return CodeBlock.builder().add("(short) $L", fieldNode.value).build(); + } + return CodeBlock.builder().add("java.lang.Byte.parseByte(\"dummy\")").build(); + case 'F': + if (fieldNode.value instanceof Float) { + return CodeBlock.builder().add("$LF", fieldNode.value).build(); + } + return CodeBlock.builder().add("java.lang.Float.parseFloat(\"dummy\")").build(); + case 'Z': + if (fieldNode.value instanceof Integer) { + return CodeBlock.builder().add("$L", ((int) fieldNode.value) != 0).build(); + } + return CodeBlock.builder().add("java.lang.Boolean.parseBoolean(\"dummy\")").build(); + } + if (desc.equals("Ljava/lang/String;") && fieldNode.value instanceof String) { + return CodeBlock.builder().add("$S", fieldNode.value).build(); + } + return CodeBlock.builder().add(desc.equals("Ljava/lang/String;") ? "java.lang.String.valueOf(\"dummy\")" : "null").build(); + } + + + private static CodeBlock.Builder printChar(CodeBlock.Builder builder, char c, int value) { + if (!Character.isValidCodePoint(value) || !Character.isDefined(value)) { + return builder.add("(char) $L", value); + } + + // See https://docs.oracle.com/javase/specs/jls/se16/html/jls-3.html#jls-EscapeSequence + // ignore space or ", just use direct in those cases + switch (c) { + case '\b': + return builder.add("'\\b'"); + case '\t': + return builder.add("'\\t'"); + case '\n': + return builder.add("'\\n'"); + case '\f': + return builder.add("'\\f'"); + case '\r': + return builder.add("'\\r'"); + case '\'': + return builder.add("'\\''"); + case '\\': + return builder.add("'\\\\'"); + } + + return builder.add("'$L'", c); + } + + private void addJavaDoc() { + mappings.addFieldDoc(builder::addJavadoc, classNode.name, fieldNode.name, fieldNode.desc); + } + + void addJavaDoc(ParameterSpec.Builder paramBuilder) { + mappings.addFieldDoc(paramBuilder::addJavadoc, classNode.name, fieldNode.name, fieldNode.desc); + } + + private void addDirectAnnotations() { + addDirectAnnotations(fieldNode.invisibleAnnotations); + addDirectAnnotations(fieldNode.visibleAnnotations); + } + + private void addDirectAnnotations(List regularAnnotations) { + if (regularAnnotations == null) { + return; + } + for (AnnotationNode annotation : regularAnnotations) { + builder.addAnnotation(parseAnnotation(annotation)); + } + } + + void addDirectAnnotations(ParameterSpec.Builder paramBuilder) { + addDirectAnnotations(paramBuilder, fieldNode.invisibleAnnotations); + addDirectAnnotations(paramBuilder, fieldNode.visibleAnnotations); + } + + private void addDirectAnnotations(ParameterSpec.Builder paramBuilder, List regularAnnotations) { + if (regularAnnotations == null) { + return; + } + for (AnnotationNode annotation : regularAnnotations) { + paramBuilder.addAnnotation(parseAnnotation(annotation)); + } + } + + TypeName calculateType() { + if (fieldNode.signature != null) { + return AnnotationAwareSignatures.parseFieldSignature(fieldNode.signature, annotations, context); + } + return parseAnnotatedType(fieldNode.desc, 0, annotations.getBank(TypeReference.newTypeReference(TypeReference.FIELD)), context).getValue(); + } + + public FieldSpec build() { + return builder.build(); + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/Main.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/Main.java new file mode 100644 index 0000000000..9ff8ae2755 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/Main.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.lang.reflect.Modifier; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import com.squareup.javapoet.JavaFile; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InnerClassNode; + +import net.fabricmc.mappingpoet.Environment.ClassNamePointer; +import net.fabricmc.mappingpoet.Environment.NestedClassInfo; + +public class Main { + + public static void main(String[] args) { + if (args.length != 3 && args.length != 4) { + System.out.println(" []"); + return; + } + Path mappings = Paths.get(args[0]); + Path inputJar = Paths.get(args[1]); + Path outputDirectory = Paths.get(args[2]); + Path librariesDir = args.length < 4 ? null : Paths.get(args[3]); + + try { + if (Files.exists(outputDirectory)) { + try (var stream = Files.walk(outputDirectory)) { + stream.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + } + + Files.createDirectories(outputDirectory); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + if (!Files.exists(mappings)) { + System.out.println("could not find mappings"); + return; + } + + if (!Files.exists(inputJar)) { + System.out.println("could not find input jar"); + return; + } + + generate(mappings, inputJar, outputDirectory, librariesDir); + } + + public static void generate(Path mappings, Path inputJar, Path outputDirectory, Path librariesDir) { + final MappingsStore mapping = new MappingsStore(mappings); + Map classes = new HashMap<>(); + forEachClass(inputJar, (classNode, environment) -> writeClass(mapping, classNode, classes, environment), librariesDir); + + for (ClassBuilder classBuilder : classes.values()) { + String name = classBuilder.getClassName(); + if (name.contains("$")) continue; + + try { + int packageEnd = classBuilder.getClassName().lastIndexOf("/"); + String pkgName = packageEnd < 0 ? "" : classBuilder.getClassName().substring(0, packageEnd).replaceAll("/", "."); + JavaFile javaFile = JavaFile.builder(pkgName, classBuilder.build()).build(); + + javaFile.writeTo(outputDirectory); + } catch (Throwable t) { + throw new RuntimeException("Failed to process class "+name, t); + } + } + } + + private static void forEachClass(Path jar, ClassNodeConsumer classNodeConsumer, Path librariesDir) { + List classes = new ArrayList<>(); + Map> supers = new HashMap<>(); + Set sealedClasses = new HashSet<>(); // their subclsses/impls need non-sealed modifier + + Map nestedClasses = new ConcurrentHashMap<>(); + Map classNames = new ConcurrentHashMap<>(); + + if (librariesDir != null) { + scanNestedClasses(classNames, nestedClasses, librariesDir); + } + + try (final JarFile jarFile = new JarFile(jar.toFile())) { + Enumeration entryEnumerator = jarFile.entries(); + + while (entryEnumerator.hasMoreElements()) { + JarEntry entry = entryEnumerator.nextElement(); + + if (entry.isDirectory() || !entry.getName().endsWith(".class")) { + continue; + } + + try (InputStream is = jarFile.getInputStream(entry)) { + ClassReader reader = new ClassReader(is); + ClassNode classNode = new ClassNode(); + reader.accept(classNode, ClassReader.SKIP_CODE); + List superNames = new ArrayList<>(); + if (classNode.superName != null && !classNode.superName.equals("java/lang/Object")) { + superNames.add(classNode.superName); + } + if (classNode.interfaces != null) { + superNames.addAll(classNode.interfaces); + } + if (!superNames.isEmpty()) { + supers.put(classNode.name, superNames); + } + + if (classNode.innerClasses != null) { + for (InnerClassNode e : classNode.innerClasses) { + if (e.outerName != null) { + // null -> declared in method/initializer + nestedClasses.put(e.name, new NestedClassInfo(e.outerName, !Modifier.isStatic(e.access), e.innerName)); + } + } + } + + if (classNode.permittedSubclasses != null) { + sealedClasses.add(classNode.name); + } + + classes.add(classNode); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + //Sort all the classes making sure that inner classes come after the parent classes + classes.sort(Comparator.comparing(o -> o.name)); + + for (ClassNode node : classes) { + classNodeConsumer.accept(node, new Environment(supers, sealedClasses, nestedClasses)); + } + } + + private static void scanNestedClasses(Map classNames, Map instanceInnerClasses, Path librariesDir) { + try { + Files.walkFileTree(librariesDir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (!file.getFileName().toString().endsWith(".jar")) { + return FileVisitResult.CONTINUE; + } + + try (final JarFile jarFile = new JarFile(file.toFile())) { + Enumeration entryEnumerator = jarFile.entries(); + + while (entryEnumerator.hasMoreElements()) { + JarEntry entry = entryEnumerator.nextElement(); + + if (entry.isDirectory() || !entry.getName().endsWith(".class")) { + continue; + } + + try (InputStream is = jarFile.getInputStream(entry)) { + ClassReader reader = new ClassReader(is); + reader.accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public void visitInnerClass(String name, String outerName, String simpleName, int access) { + instanceInnerClasses.put(name, new Environment.NestedClassInfo(outerName, !Modifier.isStatic(access), simpleName)); + if (outerName != null) { + classNames.put(name, new ClassNamePointer(simpleName, outerName)); + } + } + }, ClassReader.SKIP_CODE); + } + } + } + + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static boolean isInstanceInnerOnClasspath(String internalName) { + String javaBinary = internalName.replace('/', '.'); + + try { + Class c = Class.forName(javaBinary, false, Main.class.getClassLoader()); + return !Modifier.isStatic(c.getModifiers()) && c.getDeclaringClass() != null; + } catch (Throwable ex) { + return false; + } + } + + private static boolean isDigit(char ch) { + return ch >= '0' && ch <= '9'; + } + + private static void writeClass(MappingsStore mappings, ClassNode classNode, Map existingClasses, Environment environment) { + // TODO make sure named jar has valid InnerClasses, use that info instead + String name = classNode.name; + { + //Block anonymous class and their nested classes + int lastSearch = name.length(); + while (lastSearch != -1) { + lastSearch = name.lastIndexOf('$', lastSearch - 1); + // names starting with digit is illegal java + if (isDigit(name.charAt(lastSearch + 1))) { + return; + } + } + } + + // TODO: ensure InnerClasses is remapped, and create ClassName from parent class name + ClassBuilder classBuilder = new ClassBuilder(mappings, classNode, environment); + + if (name.contains("$")) { + String parentClass = name.substring(0, name.lastIndexOf("$")); + if (!existingClasses.containsKey(parentClass)) { + throw new RuntimeException("Could not find parent class: " + parentClass + " for " + classNode.name); + } + existingClasses.get(parentClass).addInnerClass(classBuilder); + } + + classBuilder.addMembers(); + existingClasses.put(name, classBuilder); + + } + + @FunctionalInterface + private interface ClassNodeConsumer { + void accept(ClassNode node, Environment environment); + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/MappingsStore.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/MappingsStore.java new file mode 100644 index 0000000000..84abd70f96 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/MappingsStore.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet; + +import net.fabricmc.mappingio.MappingReader; +import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; +import net.fabricmc.mappingio.format.MappingFormat; +import net.fabricmc.mappingio.tree.MappingTreeView; +import net.fabricmc.mappingio.tree.MappingTreeView.ClassMappingView; +import net.fabricmc.mappingio.tree.MappingTreeView.ElementMappingView; +import net.fabricmc.mappingio.tree.MappingTreeView.MethodMappingView; +import net.fabricmc.mappingio.tree.MemoryMappingTree; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.List; +import java.util.Map; + +import static net.fabricmc.mappingio.tree.MappingTreeView.SRC_NAMESPACE_ID; + +//Taken from loom +public class MappingsStore { + private final MappingTreeView tree; + private final int maxNamespace; + + public MappingsStore(Path tinyFile) { + this.tree = readMappings(tinyFile); + this.maxNamespace = tree.getMaxNamespaceId(); + } + + private static MappingTreeView readMappings(Path input) { + var tree = new MemoryMappingTree(); + try { + MappingReader.read(input, MappingFormat.TINY_2, new MappingSourceNsSwitch(tree, "named")); + } catch (IOException e) { + throw new RuntimeException("Failed to read mappings", e); + } + return tree; + } + + private void addDoc(ElementMappingView element, DocAdder adder) { + String doc = element.getComment(); + if (doc != null) { + adder.addJavadoc("$L", doc); + } + } + + public void addClassDoc(DocAdder adder, String className) { + var classDef = tree.getClass(className); + if (classDef == null) { + return; + } + addDoc(classDef, adder); + adder.addJavadoc("\n"); + for (int id = SRC_NAMESPACE_ID; id < maxNamespace; id++) { + String transformedName = classDef.getName(id); + adder.addJavadoc("@mapping {@literal $L:$L}\n", tree.getNamespaceName(id), transformedName); + } + } + + public void addFieldDoc(DocAdder addJavadoc, String owner, String name, String desc) { + var classDef = tree.getClass(owner); + if (classDef == null) { + return; + } + + var fieldDef = classDef.getField(name, desc); + if (fieldDef == null) { + return; + } + + addDoc(fieldDef, addJavadoc); + addJavadoc.addJavadoc("\n"); + for (int id = SRC_NAMESPACE_ID; id < maxNamespace; id++) { + String transformedName = fieldDef.getName(id); + String mixinForm = "L" + classDef.getName(id) + ";" + transformedName + ":" + fieldDef.getDesc(id); + addJavadoc.addJavadoc("@mapping {@literal $L:$L:$L}\n", tree.getNamespaceName(id), transformedName, mixinForm); + } + } + + public Map.Entry getParamNameAndDoc(Environment environment, String owner, String name, String desc, int index) { + var found = searchMethod(environment, owner, name, desc); + if (found != null) { + var methodDef = found.getValue(); + if (methodDef.getArgs().isEmpty()) { + return null; + } + return methodDef.getArgs().stream() + .filter(param -> param.getLvIndex() == index) + // Map.entry() is null-hostile + .map(param -> new SimpleImmutableEntry<>(param.getSrcName(), param.getComment())) + .findFirst() + .orElse(null); + } + return null; + } + + public void addMethodDoc(DocAdder adder, Environment environment, String owner, String name, String desc) { + var found = searchMethod(environment, owner, name, desc); + if (found == null) { + return; + } + + var methodDef = found.getValue(); + var ownerDef = found.getKey(); + if (!ownerDef.equals(methodDef.getOwner())) { + adder.addJavadoc("{@inheritDoc}"); + } else { + addDoc(methodDef, adder); + } + + adder.addJavadoc("\n"); + for (int id = SRC_NAMESPACE_ID; id < maxNamespace; id++) { + String transformedName = methodDef.getName(id); + String mixinForm = "L" + ownerDef.getName(id) + ";" + transformedName + methodDef.getDesc(id); + adder.addJavadoc("@mapping {@literal $L:$L:$L}\n", tree.getNamespaceName(id), transformedName, mixinForm); + } + } + + private Map.Entry searchMethod(Environment environment, String owner, String name, String desc) { + var classDef = tree.getClass(owner); + + if (classDef == null) + return null; + + var methodDef = classDef.getMethod(name, desc); + if (methodDef != null) + return Map.entry(methodDef.getOwner(), methodDef); + + + for (String superName : environment.superTypes().getOrDefault(owner, List.of())) { + var ret = searchMethod(environment, superName, name, desc); + if (ret != null) { + return ret; + } + } + + return null; + } + + public interface DocAdder { + void addJavadoc(String format, Object... args); + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/MethodBuilder.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/MethodBuilder.java new file mode 100644 index 0000000000..afe2bc3faf --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/MethodBuilder.java @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet; + +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.TypeName; +import net.fabricmc.mappingpoet.signature.AnnotationAwareDescriptors; +import net.fabricmc.mappingpoet.signature.AnnotationAwareSignatures; +import net.fabricmc.mappingpoet.signature.MethodSignature; +import net.fabricmc.mappingpoet.signature.TypeAnnotationBank; +import net.fabricmc.mappingpoet.signature.TypeAnnotationMapping; +import net.fabricmc.mappingpoet.signature.TypeAnnotationStorage; +import org.objectweb.asm.TypeReference; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +import javax.lang.model.element.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class MethodBuilder { + private static final Set RESERVED_KEYWORDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", + "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if", + "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private", + "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", + "throw", "throws", "transient", "try", "void", "volatile", "while" + ))); + private final MappingsStore mappings; + private final ClassNode classNode; + private final MethodNode methodNode; + private final MethodSpec.Builder builder; + private final Environment environment; + private final int formalParamStartIndex; + private final String receiverSignature; + private final TypeAnnotationMapping typeAnnotations; + + private MethodSignature signature; + + public MethodBuilder(MappingsStore mappings, ClassNode classNode, MethodNode methodNode, Environment environment, String receiverSignature, int formalParamStartIndex) { + this.mappings = mappings; + this.classNode = classNode; + this.methodNode = methodNode; + this.environment = environment; + this.receiverSignature = receiverSignature; + this.formalParamStartIndex = formalParamStartIndex; + + typeAnnotations = TypeAnnotationStorage.builder() + .add(methodNode.invisibleTypeAnnotations) + .add(methodNode.visibleTypeAnnotations) + .build(); + + this.builder = createBuilder(); + addJavaDoc(); + addAnnotations(); + setReturnType(); + addParameters(); + addExceptions(); + } + + private static void addDirectAnnotations(ParameterSpec.Builder builder, List[] regularAnnotations, int index) { + if (regularAnnotations == null || regularAnnotations.length <= index) { + return; + } + addDirectAnnotations(builder, regularAnnotations[index]); + } + + private static void addDirectAnnotations(ParameterSpec.Builder builder, List regularAnnotations) { + if (regularAnnotations == null) { + return; + } + for (AnnotationNode annotation : regularAnnotations) { + builder.addAnnotation(FieldBuilder.parseAnnotation(annotation)); + } + } + + private static IllegalArgumentException invalidMethodDesc(String desc, int index) { + return new IllegalArgumentException(String.format("Invalid method descriptor at %d: \"%s\"", index, desc)); + } + + static String reserveValidName(String suggestedName, Set usedNames) { + if (!usedNames.contains(suggestedName)) { + usedNames.add(suggestedName); + return suggestedName; + } + int t = 2; + String currentSuggestion = suggestedName + t; + while (usedNames.contains(currentSuggestion)) { + t++; + currentSuggestion = suggestedName + t; + } + + usedNames.add(currentSuggestion); + + return currentSuggestion; + } + + static String suggestName(TypeName type) { + String str = type.withoutAnnotations().toString(); + int newStart = 0; + int newEnd = str.length(); + int ltStart; + ltStart = str.indexOf('<', newStart); + if (ltStart != -1 && ltStart < newEnd) { + newEnd = ltStart; + } + ltStart = str.indexOf('[', newStart); + if (ltStart != -1 && ltStart < newEnd) { + newEnd = ltStart; + } + int dotEnd; + if ((dotEnd = str.lastIndexOf(".", newEnd)) != -1) { + newStart = dotEnd + 1; + } + str = Character.toLowerCase(str.charAt(newStart)) + str.substring(newStart + 1, newEnd); + + if (str.equals("boolean")) { + str = "bool"; + } + return str; + } + + private MethodSpec.Builder createBuilder() { + MethodSpec.Builder builder = MethodSpec.methodBuilder(methodNode.name) + .addModifiers(new ModifierBuilder(methodNode.access).getModifiers(ModifierBuilder.Type.METHOD)); + if (methodNode.name.equals("") || !java.lang.reflect.Modifier.isInterface(classNode.access) || java.lang.reflect.Modifier.isPrivate(methodNode.access)) { + builder.modifiers.remove(Modifier.DEFAULT); + } + + if (methodNode.signature != null) { + signature = AnnotationAwareSignatures.parseMethodSignature(methodNode.signature, typeAnnotations, environment); + builder.addTypeVariables(signature.generics()); + } + + return builder; + } + + private void addAnnotations() { + addDirectAnnotations(methodNode.invisibleAnnotations); + addDirectAnnotations(methodNode.visibleAnnotations); + } + + private void addDirectAnnotations(List regularAnnotations) { + if (regularAnnotations == null) { + return; + } + for (AnnotationNode annotation : regularAnnotations) { + builder.addAnnotation(FieldBuilder.parseAnnotation(annotation)); + } + } + + private void setReturnType() { + //Skip constructors + if (methodNode.name.equals("")) { + return; + } + + TypeName typeName; + if (signature != null) { + typeName = signature.result(); + } else { + String returnDesc = methodNode.desc.substring(methodNode.desc.lastIndexOf(")") + 1); + typeName = AnnotationAwareDescriptors.parseDesc(returnDesc, typeAnnotations.getBank(TypeReference.newTypeReference(TypeReference.METHOD_RETURN)), environment); + } + + builder.returns(typeName); + if (typeName != TypeName.VOID && !builder.modifiers.contains(Modifier.ABSTRACT)) { + builder.addStatement("throw new RuntimeException()"); + } else if (methodNode.annotationDefault != null) { + builder.defaultValue(FieldBuilder.codeFromAnnoValue(methodNode.annotationDefault)); + } + } + + private void addParameters(MethodBuilder this) { + // todo fix enum ctors + List paramTypes = new ArrayList<>(); + boolean instanceMethod = !builder.modifiers.contains(Modifier.STATIC); + Set usedParamNames = new HashSet<>(RESERVED_KEYWORDS); + getParams(paramTypes, instanceMethod, usedParamNames); + + // generate receiver param for type annos + + TypeAnnotationBank receiverAnnos = typeAnnotations.getBank(TypeReference.newTypeReference(TypeReference.METHOD_RECEIVER)); + if (!receiverAnnos.isEmpty()) { + ParameterSpec.Builder receiverBuilder; + // only instance inner class ctor can have receivers + if (methodNode.name.equals("")) { + TypeName annotatedReceiver = AnnotationAwareSignatures.parseSignature("L" + receiverSignature.substring(0, receiverSignature.lastIndexOf('.')) + ";", receiverAnnos, environment); + // vulnerable heuristics + String simpleNameChain = classNode.name.substring(classNode.name.lastIndexOf('/') + 1); + int part1 = simpleNameChain.lastIndexOf('$'); // def exists + int part2 = simpleNameChain.lastIndexOf('$', part1 - 1); // may be -1 + String usedName = simpleNameChain.substring(part2 + 1, part1); + receiverBuilder = ParameterSpec.builder(annotatedReceiver, usedName + ".this"); + } else { + TypeName annotatedReceiver = AnnotationAwareSignatures.parseSignature("L" + receiverSignature + ";", receiverAnnos, environment); + receiverBuilder = ParameterSpec.builder(annotatedReceiver, "this"); + } + // receiver param cannot have its jd/param anno except type use anno + builder.addParameter(receiverBuilder.build()); + } + + List[] visibleParameterAnnotations = methodNode.visibleParameterAnnotations; + List[] invisibleParameterAnnotations = methodNode.invisibleParameterAnnotations; + int index = 0; + for (ParamType paramType : paramTypes) { + paramType.fillName(usedParamNames); + ParameterSpec.Builder paramBuilder = ParameterSpec.builder(paramType.type, paramType.name, paramType.modifiers); + if (paramType.comment != null) { + paramBuilder.addJavadoc(paramType.comment + "\n"); + } + addDirectAnnotations(paramBuilder, visibleParameterAnnotations, index); + addDirectAnnotations(paramBuilder, invisibleParameterAnnotations, index); + builder.addParameter(paramBuilder.build()); + index++; + } + } + + private void getParams(List paramTypes, boolean instance, Set usedParamNames) { + int slot = instance ? 1 : 0; + final String desc = methodNode.desc; + int paramIndex = 0; + int index = 0; + + if (desc.charAt(index) != '(') { + throw invalidMethodDesc(desc, index); + } + index++; // consume '(' + + Iterator signatureParamIterator = signature == null ? Collections.emptyIterator() : signature.parameters().iterator(); + while (desc.charAt(index) != ')') { + int oldIndex = index; + Map.Entry parsedParam = FieldBuilder.parseType(desc, index); + index = parsedParam.getKey(); + TypeName nonAnnotatedParsedType = parsedParam.getValue(); + + if (paramIndex >= formalParamStartIndex) { // skip guessed synthetic/implicit params + TypeName parsedType; + if (signatureParamIterator.hasNext()) { + parsedType = signatureParamIterator.next(); + } else { + parsedType = AnnotationAwareDescriptors.parseDesc(desc.substring(oldIndex, index), typeAnnotations.getBank(TypeReference.newFormalParameterReference(paramIndex - formalParamStartIndex)), environment); + } + paramTypes.add(new ParamType(mappings.getParamNameAndDoc(environment, classNode.name, methodNode.name, methodNode.desc, slot), parsedType, usedParamNames, slot)); + } + slot++; + if (nonAnnotatedParsedType.equals(TypeName.DOUBLE) || nonAnnotatedParsedType.equals(TypeName.LONG)) { + slot++; + } + paramIndex++; + } + /* bruh, we don't care about return type + index++; // consume ')' + Map.Entry parsedReturn = FieldBuilder.parseType(desc, index); + index = parsedReturn.getKey(); + TypeName returnType = parsedReturn.getValue(); + */ + } + + private void addExceptions() { + if (signature != null && !signature.thrown().isEmpty()) { + for (TypeName each : signature.thrown()) { + builder.addException(each); + } + return; + } + List exceptions = methodNode.exceptions; + if (exceptions != null) { + int index = 0; + for (String internalName : exceptions) { + builder.addException(AnnotationAwareDescriptors.parseType(internalName, typeAnnotations.getBank(TypeReference.newExceptionReference(index)), environment)); + index++; + } + } + } + + private void addJavaDoc() { + mappings.addMethodDoc(builder::addJavadoc, environment, classNode.name, methodNode.name, methodNode.desc); + } + + public MethodSpec build() { + return builder.build(); + } + + private class ParamType { + final String comment; + private final TypeName type; + private final Modifier[] modifiers; + private String name; + + public ParamType(Map.Entry nameAndDoc, TypeName type, Set usedNames, int slot) { + this.name = nameAndDoc != null ? nameAndDoc.getKey() : null; + if (this.name != null) { + if (usedNames.contains(this.name)) { + System.err.printf("Overridden parameter name detected in %s %s %s slot %d, resetting%n", classNode.name, methodNode.name, methodNode.desc, slot); + this.name = null; + } else { + usedNames.add(this.name); + } + } + this.comment = nameAndDoc == null ? null : nameAndDoc.getValue(); + this.type = type; + this.modifiers = new ModifierBuilder(0) + .getModifiers(ModifierBuilder.Type.PARAM); + } + + private void fillName(Set usedNames) { + if (name != null) { + return; + } + name = reserveValidName(suggestName(type), usedNames); + } + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/ModifierBuilder.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/ModifierBuilder.java new file mode 100644 index 0000000000..926ee94895 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/ModifierBuilder.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet; + +import org.objectweb.asm.tree.ClassNode; + +import javax.lang.model.element.Modifier; +import java.util.ArrayList; +import java.util.List; + +public class ModifierBuilder { + + private final int access; + private boolean needsUnseal; + + public ModifierBuilder(int access) { + this.access = access; + } + + public ModifierBuilder checkUnseal(ClassNode node, Environment env) { + if (java.lang.reflect.Modifier.isFinal(node.access)) { + return this; + } + + if (node.interfaces != null) { + for (String itf : node.interfaces) { + if (env.sealedClasses().contains(itf)) { + needsUnseal = true; + return this; + } + } + } + + if (node.superName != null && env.sealedClasses().contains(node.superName)) { + needsUnseal = true; + } + + + return this; + } + + public Modifier[] getModifiers(Type type) { + List modifiers = new ArrayList<>(); + + if (type == Type.PARAM) { + if (java.lang.reflect.Modifier.isFinal(access)) { + modifiers.add(Modifier.FINAL); + } + return modifiers.toArray(new Modifier[]{}); + } + + if (java.lang.reflect.Modifier.isPublic(access)) { + modifiers.add(Modifier.PUBLIC); + } else if (java.lang.reflect.Modifier.isPrivate(access)) { + modifiers.add(Modifier.PRIVATE); + } else if (java.lang.reflect.Modifier.isProtected(access)) { + modifiers.add(Modifier.PROTECTED); + } + + if (java.lang.reflect.Modifier.isAbstract(access) && type != Type.ENUM) { + modifiers.add(Modifier.ABSTRACT); + } + if (java.lang.reflect.Modifier.isStatic(access)) { + modifiers.add(Modifier.STATIC); + } + if (!java.lang.reflect.Modifier.isAbstract(access) && !java.lang.reflect.Modifier.isStatic(access) && type == Type.METHOD) { + modifiers.add(Modifier.DEFAULT); + } + + if (java.lang.reflect.Modifier.isFinal(access) && type != Type.ENUM && type != Type.RECORD) { + modifiers.add(Modifier.FINAL); + } + if (java.lang.reflect.Modifier.isTransient(access) && type == Type.FIELD) { + modifiers.add(Modifier.TRANSIENT); + } + if (java.lang.reflect.Modifier.isVolatile(access) && type == Type.FIELD) { + modifiers.add(Modifier.VOLATILE); + } + if (java.lang.reflect.Modifier.isSynchronized(access) && type == Type.METHOD) { + modifiers.add(Modifier.SYNCHRONIZED); + } + if (java.lang.reflect.Modifier.isNative(access) && type == Type.METHOD) { + modifiers.add(Modifier.NATIVE); + } + if (java.lang.reflect.Modifier.isStrict(access)) { + modifiers.add(Modifier.STRICTFP); // obsolete as of Java 17 + } + + if (needsUnseal && type == Type.CLASS) { + modifiers.add(Modifier.NON_SEALED); + } + + return modifiers.toArray(new Modifier[]{}); + } + + public static Type getType(boolean enumType, boolean recordType) { + return enumType ? Type.ENUM : recordType ? Type.RECORD : Type.CLASS; + } + + public enum Type { + CLASS, + ENUM, + RECORD, + METHOD, + FIELD, + PARAM + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/Signatures.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/Signatures.java new file mode 100644 index 0000000000..c2b996879c --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/Signatures.java @@ -0,0 +1,488 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeVariableName; +import com.squareup.javapoet.WildcardTypeName; + +import net.fabricmc.mappingpoet.signature.ClassSignature; +import net.fabricmc.mappingpoet.signature.MethodSignature; + +public final class Signatures { + + public static ClassSignature parseClassSignature(final String signature) { + // ;B:Ljava/lang/Object>Ljava/lang/Object; etc etc + int index = 0; + char ch; + List generics = Collections.emptyList(); + if (signature.charAt(0) == '<') { + // parse generic decl + index++; // consume '<' + + // parse type params e.g. + generics = new LinkedList<>(); + while ((ch = signature.charAt(index)) != '>') { + int genericNameStart = index; + if (ch == ':') { + throw errorAt(signature, index); + } + do { + index++; + } while (signature.charAt(index) != ':'); + + String genericName = signature.substring(genericNameStart, index); + + List bounds = new LinkedList<>(); + boolean classBound = true; + while (signature.charAt(index) == ':') { + // parse bounds + index++; // consume ':' + if (classBound && signature.charAt(index) == ':') { + // No class bound, only interface bounds, so '::' + classBound = false; + continue; + } + classBound = false; + Map.Entry bound = parseParameterizedType(signature, index); + index = bound.getKey(); + bounds.add(bound.getValue()); + } + + generics.add(TypeVariableName.get(genericName, bounds.toArray(new TypeName[0]))); + } + + index++; // consume '>' + } + + LinkedList supers = new LinkedList<>(); + while (index < signature.length()) { + Map.Entry bound = parseParameterizedType(signature, index); + index = bound.getKey(); + supers.add(bound.getValue()); + } + + return new ClassSignature(generics, supers.removeFirst(), supers); + } + + public static MethodSignature parseMethodSignature(String signature) { + int index = 0; + char ch; + List generics = Collections.emptyList(); + if (signature.charAt(0) == '<') { + // parse generic decl + index++; // consume '<' + + // parse type params e.g. + generics = new LinkedList<>(); + while ((ch = signature.charAt(index)) != '>') { + int genericNameStart = index; + if (ch == ':') { + throw errorAt(signature, index); + } + do { + index++; + } while (signature.charAt(index) != ':'); + + String genericName = signature.substring(genericNameStart, index); + + List bounds = new LinkedList<>(); + boolean classBound = true; + while (signature.charAt(index) == ':') { + // parse bounds + index++; // consume ':' + if (classBound && signature.charAt(index) == ':') { + // No class bound, only interface bounds, so '::' + classBound = false; + continue; + } + classBound = false; + Map.Entry bound = parseParameterizedType(signature, index); + index = bound.getKey(); + bounds.add(bound.getValue()); + } + + generics.add(TypeVariableName.get(genericName, bounds.toArray(new TypeName[0]))); + } + + index++; // consume '>' + } + + if (signature.charAt(index) != '(') { + throw errorAt(signature, index); + } + index++; // consume '(' + + LinkedList params = new LinkedList<>(); + while (signature.charAt(index) != ')') { + Map.Entry param = parseParameterizedType(signature, index); + index = param.getKey(); + params.add(param.getValue()); + } + + index++; // consume ')' + + TypeName returnType; + if (signature.charAt(index) == 'V') { + returnType = TypeName.VOID; + index++; + } else { + Map.Entry parsedReturnType = parseParameterizedType(signature, index); + index = parsedReturnType.getKey(); + returnType = parsedReturnType.getValue(); + } + + LinkedList thrown = new LinkedList<>(); + while (index < signature.length() && signature.charAt(index) == '^') { + index++; // consume '^' + Map.Entry parsedThrown = parseParameterizedType(signature, index); + index = parsedThrown.getKey(); + thrown.addLast(parsedThrown.getValue()); + } + + return new MethodSignature(generics, params, returnType, thrown); + } + + public static TypeName parseFieldSignature(String signature) { + return parseParameterizedType(signature, 0).getValue(); + } + + public static Map.Entry parseParameterizedType(final String signature, final int startOffset) { + GenericStack stack = new GenericStack(); + + int index = startOffset; + // the loop parses a type and try to quit levels if possible + do { + char ch = signature.charAt(index); + boolean parseExactType = true; + boolean bounded = false; + boolean extendsBound = false; + + switch (ch) { + case '*': { + index++; + parseExactType = false; + stack.addWildcard(); + break; + } + case '+': { + index++; + bounded = true; + extendsBound = true; + break; + } + case '-': { + index++; + bounded = true; + extendsBound = false; + break; + } + default: { + // do nothing + } + } + + if (parseExactType) { + int arrayLevel = 0; + while ((ch = signature.charAt(index)) == '[') { + index++; + arrayLevel++; + } + + index++; // whatever the prefix is it's consumed + switch (ch) { + case 'B': + case 'C': + case 'D': + case 'F': + case 'I': + case 'J': + case 'S': + case 'Z': { + // primitives + stack.add(getPrimitive(ch), arrayLevel, bounded, extendsBound); + break; + } + case 'T': { + // "TE;" for + int nameStart = index; + while (signature.charAt(index) != ';') { + index++; + } + String typeVarName = signature.substring(nameStart, index); + stack.add(TypeVariableName.get(typeVarName), arrayLevel, bounded, extendsBound); + index++; // read ending ";" + break; + } + case 'L': { + // Lcom/example/Outer.Inner; + // Lcom/example/Outer$Inner; + // dot only appears after ">"! + int nameStart = index; + ClassName currentClass = null; + int nextSimpleNamePrev = -1; + do { + ch = signature.charAt(index); + + if (ch == '/') { + if (currentClass != null) { + throw errorAt(signature, index); + } + nextSimpleNamePrev = index; + } + + if (ch == '$' || ch == '<' || ch == ';') { + if (currentClass == null) { + String packageName = nextSimpleNamePrev == -1 ? "" : signature.substring(nameStart, nextSimpleNamePrev).replace('/', '.'); + String simpleName = signature.substring(nextSimpleNamePrev + 1, index); + currentClass = ClassName.get(packageName, simpleName); + } else { + String simpleName = signature.substring(nextSimpleNamePrev + 1, index); + currentClass = currentClass.nestedClass(simpleName); + } + nextSimpleNamePrev = index; + } + + index++; + } while (ch != '<' && ch != ';'); + + assert currentClass != null; + if (ch == ';') { + stack.add(currentClass, arrayLevel, bounded, extendsBound); + } + + if (ch == '<') { + stack.push(Frame.ofClass(currentClass), arrayLevel, bounded, extendsBound); + } + break; + } + default: { + throw errorAt(signature, index); + } + } + } + + // quit generics + quitLoop: + while (stack.canQuit() && signature.charAt(index) == '>') { + // pop + stack.popFrame(); + index++; + + // followups like .B in A.B + if ((ch = signature.charAt(index)) != ';') { + if (ch != '.') { + throw errorAt(signature, index); + } + index++; + int innerNameStart = index; + final int checkIndex = index; + stack.checkHead(head -> { + if (!(head instanceof ParameterizedTypeName)) { + throw errorAt(signature, checkIndex); + } + }); + + while (true) { + ch = signature.charAt(index); + if (ch == '.' || ch == ';' || ch == '<') { + String simpleName = signature.substring(innerNameStart, index); + if (ch == '.' || ch == ';') { + stack.tweakLast(name -> ((ParameterizedTypeName) name).nestedClass(simpleName)); + if (ch == ';') { + index++; + break; + } + } else { + stack.push(Frame.ofGenericInnerClass((ParameterizedTypeName) stack.deque.getLast().typeNames.removeLast(), simpleName)); + index++; + break quitLoop; + } + } + + index++; + } + } else { + index++; + } + + } + + } while (stack.canQuit()); + + assert stack.deque.size() == 1; + assert stack.deque.getLast().typeNames.size() == 1; + return new AbstractMap.SimpleImmutableEntry<>(index, stack.collectFrame()); + } + + private static IllegalArgumentException errorAt(String signature, int index) { + return new IllegalArgumentException(String.format("Signature format error at %d for \"%s\"", index, signature)); + } + + public static TypeName wrap(TypeName component, int level, boolean bounded, boolean extendsBound) { + TypeName ret = component; + for (int i = 0; i < level; i++) { + ret = ArrayTypeName.of(ret); + } + return bounded ? extendsBound ? WildcardTypeName.subtypeOf(ret) : WildcardTypeName.supertypeOf(ret) : ret; + } + + public static TypeName getPrimitive(char c) { + switch (c) { + case 'B': + return TypeName.BYTE; + case 'C': + return TypeName.CHAR; + case 'D': + return TypeName.DOUBLE; + case 'F': + return TypeName.FLOAT; + case 'I': + return TypeName.INT; + case 'J': + return TypeName.LONG; + case 'S': + return TypeName.SHORT; + case 'V': + return TypeName.VOID; + case 'Z': + return TypeName.BOOLEAN; + } + throw new IllegalArgumentException("Invalid primitive " + c); + } + + @FunctionalInterface + interface Frame { + static Frame ofClass(ClassName className) { + return parameters -> ParameterizedTypeName.get(className, parameters.toArray(new TypeName[0])); + } + + static Frame ofGenericInnerClass(ParameterizedTypeName outerClass, String innerName) { + return parameters -> outerClass.nestedClass(innerName, parameters); + } + + static Frame collecting() { + return parameters -> { + if (parameters.size() != 1) { + throw new IllegalStateException(); + } + return parameters.get(0); + }; + } + + T acceptParameters(List parameters); + } + + @FunctionalInterface + interface HeadChecker { + void check(TypeName typeName) throws E; + } + + static final class GenericStack { + private final Deque deque = new LinkedList<>(); + + GenericStack() { + deque.addLast(new Element(Frame.collecting())); + } + + public boolean canQuit() { + return deque.size() > 1; + } + + public void push(Frame frame) { + deque.addLast(new Element(frame)); + } + + public void push(Frame frame, int arrayLevel, boolean bounded, boolean extendsBound) { + deque.getLast().pushAttributes(arrayLevel, bounded, extendsBound); + push(frame); + } + + public void addWildcard() { + add(WildcardTypeName.subtypeOf(ClassName.OBJECT), 0, false, false); + } + + public void add(TypeName typeName, int arrayLevel, boolean bounded, boolean extendsBound) { + deque.getLast().add(typeName, arrayLevel, bounded, extendsBound); + } + + public void tweakLast(UnaryOperator modifier) { + LinkedList typeNames = deque.getLast().typeNames; + typeNames.addLast(modifier.apply(typeNames.removeLast())); + } + + public TypeName collectFrame() { + return deque.removeLast().pop(); + } + + public void popFrame() { + TypeName name = collectFrame(); + deque.getLast().typeNames.addLast(name); + } + + public void checkHead(HeadChecker checker) throws E { + checker.check(deque.getLast().typeNames.getLast()); + } + + private static final class Element { + final LinkedList typeNames; + final Frame frame; + int arrayLevel = 0; + boolean bounded = false; + boolean extendsBound = false; + + Element(Frame frame) { + this.typeNames = new LinkedList<>(); + this.frame = frame; + } + + void add(TypeName typeName, int arrayLevel, boolean bounded, boolean extendsBound) { + pushAttributes(arrayLevel, bounded, extendsBound); + typeNames.addLast(typeName); + } + + private void updateLast() { + if (!typeNames.isEmpty()) { + TypeName lastTypeName = typeNames.removeLast(); + typeNames.addLast(Signatures.wrap(lastTypeName, this.arrayLevel, this.bounded, this.extendsBound)); + } + } + + void pushAttributes(int arrayLevel, boolean bounded, boolean extendsBound) { + updateLast(); + this.arrayLevel = arrayLevel; + this.bounded = bounded; + this.extendsBound = extendsBound; + } + + TypeName pop() { + updateLast(); + return frame.acceptParameters(typeNames); + } + } + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/jd/MappingTaglet.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/jd/MappingTaglet.java new file mode 100644 index 0000000000..08bc6a36d7 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/jd/MappingTaglet.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet.jd; + +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +import com.sun.source.doctree.DocTree; +import com.sun.source.doctree.LiteralTree; +import com.sun.source.doctree.UnknownBlockTagTree; +import jdk.javadoc.doclet.Taglet; + +@SuppressWarnings("unused") +public final class MappingTaglet implements Taglet { + + public MappingTaglet() { + // Required by javadoc + } + + @Override + public Set getAllowedLocations() { + return EnumSet.of(Location.TYPE, Location.CONSTRUCTOR, Location.METHOD, Location.FIELD); + } + + @Override + public boolean isInlineTag() { + return false; + } + + @Override + public String getName() { + return "mapping"; + } + + @Override + public String toString(List tags, Element element) { + boolean typeDecl = element instanceof TypeElement; // means it's a class, itf, enum, etc. + StringBuilder builder = new StringBuilder(); + builder.append("
Mappings:
\n"); + builder.append("
\n"); + builder.append("\n"); + builder.append("\n"); + builder.append("\n"); + if (!typeDecl) { + builder.append("\n"); + } + builder.append("\n"); + builder.append("\n"); + + for (DocTree each : tags) { + String body = ((UnknownBlockTagTree) each).getContent().stream().map(t -> ((LiteralTree) t).getBody().getBody()).collect(Collectors.joining()); + String[] ans = body.split(":", 3); + builder.append("\n"); + builder.append(String.format("\n", escaped(ans[0]))); + builder.append(String.format("\n", escaped(ans[1]))); + if (!typeDecl) { + builder.append(String.format("\n", escaped(ans[2]))); + } + builder.append("\n"); + } + + builder.append("\n"); + builder.append("
NamespaceNameMixin selector
%s%s%s
\n"); + return builder.toString(); + } + + // I hate + private static String escaped(String original) { + StringBuilder builder = new StringBuilder(original.length()); + final int len = original.length(); + for (int i = 0; i < len; i++) { + char c = original.charAt(i); + if (c > 127 || c == '"' || c == '\'' || c == '<' || c == '>' || c == '&') { + builder.append("&#").append((int) c).append(";"); + } else { + builder.append(c); + } + } + return builder.toString(); + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/AnnotationAwareDescriptors.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/AnnotationAwareDescriptors.java new file mode 100644 index 0000000000..c845c8d656 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/AnnotationAwareDescriptors.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet.signature; + +import java.util.AbstractMap; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.TypeName; +import org.objectweb.asm.TypePath; +import org.objectweb.asm.TypeReference; + +import net.fabricmc.mappingpoet.Signatures; + +public final class AnnotationAwareDescriptors { + + private AnnotationAwareDescriptors() { + } + + // not really signature, but annotated classes + public static ClassSignature parse(String rawSuper, List rawInterfaces, TypeAnnotationMapping mapping, ClassStaticContext context) { + ClassName superName = parseType(rawSuper, mapping.getBank(TypeReference.newSuperTypeReference(-1)), context); + + List interfaces = new ArrayList<>(rawInterfaces.size()); + for (ListIterator itr = rawInterfaces.listIterator(); itr.hasNext(); ) { + int i = itr.nextIndex(); + String item = itr.next(); + + ClassName itfName = parseType(item, mapping.getBank(TypeReference.newSuperTypeReference(i)), context); + interfaces.add(itfName); + } + + return new ClassSignature(Collections.emptyList(), superName, interfaces); + } + + // only for descriptor-based ones. Use signature visitor for signature-based ones! + public static TypeName parseDesc(String desc, TypeAnnotationBank bank, ClassStaticContext context) { + Deque> arrayAnnotations = new ArrayDeque<>(); + int len = desc.length(); + int index; + for (index = 0; (index < len) && (desc.charAt(index) == '['); index++) { + arrayAnnotations.push(bank.getCurrentAnnotations()); + bank = bank.advance(TypePath.ARRAY_ELEMENT, 0); + } + + TypeName current; + if (len - index == 1) { + current = annotate(Signatures.getPrimitive(desc.charAt(index)), bank); + } else { + // L ; + assert desc.charAt(index) == 'L' && desc.charAt(len - 1) == ';'; + current = parseType(desc.substring(index + 1, len - 1), bank, context); + } + while (!arrayAnnotations.isEmpty()) { + current = ArrayTypeName.of(current); + List specs = arrayAnnotations.pop(); + if (!specs.isEmpty()) { + current = current.annotated(specs); + } + } + + return current; + } + + public static ClassName parseType(String internalName, TypeAnnotationBank bank, ClassStaticContext context) { + Map.Entry result = annotateUpTo(internalName, bank, context); + return annotate(result.getKey(), result.getValue()); + } + + /** + * Annotate class name chains until the last element. Useful for making the last + * element a parameterized type before annotating it. + * + * @param internalName the internal name chain + * @param bank the annotation storage + * @param context the context for testing instance inner class + * @return the class name ready for parameterization/annotation and the current annotation state + */ + static Map.Entry annotateUpTo(String internalName, TypeAnnotationBank bank, ClassStaticContext context) { + if (internalName.startsWith("L") && internalName.endsWith(";")) { + throw new AssertionError(internalName); + } + int slice = internalName.lastIndexOf('/'); + String packageSt = slice < 0 ? "" : internalName.substring(0, slice).replace('/', '.'); + + int moneySign = internalName.indexOf('$', slice + 1); + if (moneySign == -1) { + return new AbstractMap.SimpleImmutableEntry<>(ClassName.get(packageSt, internalName.substring(slice + 1)), bank); + } + + ClassName current = ClassName.get(packageSt, internalName.substring(slice + 1, moneySign)); + + final int len = internalName.length(); + boolean enteredInner = false; + for (int i = moneySign; i < len; ) { + int t = internalName.indexOf('$', i + 1); + if (t < 0) { + t = len; + } + + // do work + if (!enteredInner && context.isInstanceInner(internalName.substring(0, t))) { + enteredInner = true; // instance inner classes cannot nest static ones + } + + if (enteredInner) { + // annotate parent before we advance + current = annotate(current, bank); + } + + current = current.nestedClass(internalName.substring(i + 1, t)); + + if (enteredInner) { + // advance on path as it's instance inner class + bank = bank.advance(TypePath.INNER_TYPE, 0); + } + + i = t; + } + + return new AbstractMap.SimpleImmutableEntry<>(current, bank); + } + + @SuppressWarnings("unchecked") + public static T annotate(T input, TypeAnnotationBank storage) { + List annotations = storage.getCurrentAnnotations(); + return annotations.isEmpty() ? input : (T) input.annotated(annotations); // it's implemented so + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/AnnotationAwareSignatures.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/AnnotationAwareSignatures.java new file mode 100644 index 0000000000..d762dfc0a5 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/AnnotationAwareSignatures.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet.signature; + +import com.squareup.javapoet.TypeName; +import org.objectweb.asm.TypeReference; +import org.objectweb.asm.signature.SignatureReader; + +public final class AnnotationAwareSignatures { + + private AnnotationAwareSignatures() { + } + + public static ClassSignature parseClassSignature(String signature, TypeAnnotationMapping annotationMapping, ClassStaticContext context) { + PoetClassMethodSignatureVisitor visitor = new PoetClassMethodSignatureVisitor(annotationMapping, context, true); + new SignatureReader(signature).accept(visitor); + return visitor.collectClass(); + } + + // Note: No receiver (self) parameter included! + public static MethodSignature parseMethodSignature(String signature, TypeAnnotationMapping annotationMapping, ClassStaticContext context) { + PoetClassMethodSignatureVisitor visitor = new PoetClassMethodSignatureVisitor(annotationMapping, context, false); + new SignatureReader(signature).accept(visitor); + return visitor.collectMethod(); + } + + public static TypeName parseFieldSignature(String signature, TypeAnnotationMapping annotationMapping, ClassStaticContext context) { + return parseSignature(signature, annotationMapping.getBank(TypeReference.newTypeReference(TypeReference.FIELD)), context); + } + + public static TypeName parseSignature(String signature, TypeAnnotationBank annotations, ClassStaticContext context) { + PoetTypeSignatureWriter visitor = new PoetTypeSignatureWriter(annotations, context); + new SignatureReader(signature).acceptType(visitor); + return visitor.compute(); + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/ClassSignature.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/ClassSignature.java new file mode 100644 index 0000000000..ab15942dcc --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/ClassSignature.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet.signature; + +import java.util.List; + +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeVariableName; + +// no more a class signature but general super info about class +public record ClassSignature(List generics, TypeName superclass, + List superinterfaces) { + +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/ClassStaticContext.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/ClassStaticContext.java new file mode 100644 index 0000000000..22e0536280 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/ClassStaticContext.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet.signature; + +/** + * A context to retrieve if a class is an instance inner class. Useful for + * placing type annotations correctly. See + * + * an example in JVM Specification 15. + */ +public interface ClassStaticContext { + + /** + * Returns if this class is an instance inner class. + * + *

For example, a top-level class is not so. A static nested + * class, such as {@code Map.Entry}, is not as well.

+ * + * @param internalName the JVM name of the class + * @return whether this class is not an instance inner class. + */ + boolean isInstanceInner(String internalName); +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/MethodSignature.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/MethodSignature.java new file mode 100644 index 0000000000..bea2f7222e --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/MethodSignature.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet.signature; + +import java.util.List; + +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeVariableName; + +public record MethodSignature(List generics, + List parameters, TypeName result, + List thrown) { + +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/PoetClassMethodSignatureVisitor.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/PoetClassMethodSignatureVisitor.java new file mode 100644 index 0000000000..a65c6ae575 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/PoetClassMethodSignatureVisitor.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet.signature; + +import java.util.ArrayList; + +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeVariableName; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.TypeReference; +import org.objectweb.asm.signature.SignatureVisitor; + +public final class PoetClassMethodSignatureVisitor extends SignatureVisitor { + + private final TypeAnnotationMapping mapping; + private final ClassStaticContext context; + private final boolean forClass; + ArrayList generics = new ArrayList<>(); + // collecting generic + String currentGenericName; + ArrayList currentGenericBounds = new ArrayList<>(); + // bound for each generic + PoetTypeSignatureWriter pendingLowerBound; + + // classes usage + ArrayList superTypes = new ArrayList<>(); + PoetTypeSignatureWriter pendingSupertype; + + // methods usage + ArrayList params = new ArrayList<>(); + ArrayList throwables = new ArrayList<>(); + PoetTypeSignatureWriter pendingSlot; + TypeName returnType; + + public PoetClassMethodSignatureVisitor(TypeAnnotationMapping mapping, ClassStaticContext context, boolean forClass) { + super(Opcodes.ASM9); + this.mapping = mapping; + this.context = context; + this.forClass = forClass; + } + + private void collectGeneric() { + collectLowerBound(); + + if (currentGenericName != null) { + TypeVariableName generic = TypeVariableName.get(currentGenericName, currentGenericBounds.toArray(new TypeName[0])); + TypeAnnotationBank bank = mapping.getBank(TypeReference.newTypeParameterReference(forClass ? TypeReference.CLASS_TYPE_PARAMETER : TypeReference.METHOD_TYPE_PARAMETER, generics.size())); + generic = AnnotationAwareDescriptors.annotate(generic, bank); + generics.add(generic); + + currentGenericName = null; + currentGenericBounds.clear(); + } + } + + private void collectGenerics() { + // end all generics + collectGeneric(); + } + + // starts a new generic declaration, like in " T[] toArray(T[] input);" + @Override + public void visitFormalTypeParameter(String name) { + collectGeneric(); + // collect existing type parameter + // start type var name + currentGenericName = name; + currentGenericBounds.clear(); + } + + private void collectLowerBound() { + if (pendingLowerBound != null) { + currentGenericBounds.add(pendingLowerBound.compute()); + pendingLowerBound = null; + } + } + + private SignatureVisitor visitLowerBound() { + collectLowerBound(); + + TypeAnnotationBank bank = mapping.getBank(TypeReference.newTypeParameterBoundReference(forClass ? + TypeReference.CLASS_TYPE_PARAMETER_BOUND : TypeReference.METHOD_TYPE_PARAMETER_BOUND, generics.size(), + currentGenericBounds.size())); + return pendingLowerBound = new PoetTypeSignatureWriter(bank, context); + } + + @Override + public SignatureVisitor visitClassBound() { + return visitLowerBound(); + } + + @Override + public SignatureVisitor visitInterfaceBound() { + return visitLowerBound(); + } + + // class exclusive + + private void collectSupertype() { + if (pendingSupertype != null) { + TypeName simple = pendingSupertype.compute(); + superTypes.add(simple); + + pendingSupertype = null; + } + } + + // always called + @Override + public SignatureVisitor visitSuperclass() { + collectGenerics(); + // don't need to collect other supertype + + return pendingSupertype = new PoetTypeSignatureWriter(mapping.getBank(TypeReference.newSuperTypeReference(-1)), context); + } + + @Override + public SignatureVisitor visitInterface() { + // super class always visited, no generic check + collectSupertype(); + + return pendingSupertype = new PoetTypeSignatureWriter(mapping.getBank(TypeReference.newSuperTypeReference(superTypes.size() - 1)), context); + } + + public ClassSignature collectClass() { + collectSupertype(); + + TypeName superclass = superTypes.remove(0); + return new ClassSignature(generics, superclass, superTypes); + } + + // method exclusive + + private void collectParam() { + if (pendingSlot != null) { + TypeName slot = pendingSlot.compute(); + params.add(slot); + + pendingSlot = null; + } + } + + private void collectReturnOrThrows() { + if (pendingSlot != null) { + if (returnType == null) { + returnType = pendingSlot.compute(); + } else { + throwables.add(pendingSlot.compute()); + } + + pendingSlot = null; + } + } + + @Override + public SignatureVisitor visitParameterType() { + collectGenerics(); + collectParam(); + + return pendingSlot = new PoetTypeSignatureWriter(mapping.getBank(TypeReference.newFormalParameterReference(params.size())), context); + } + + @Override + public SignatureVisitor visitReturnType() { + collectGenerics(); // they may skip visiting params, rip! + collectParam(); + + return pendingSlot = new PoetTypeSignatureWriter(mapping.getBank(TypeReference.newTypeReference(TypeReference.METHOD_RETURN)), context); + } + + @Override + public SignatureVisitor visitExceptionType() { + collectReturnOrThrows(); + + return pendingSlot = new PoetTypeSignatureWriter(mapping.getBank(TypeReference.newExceptionReference(throwables.size())), context); + } + + public MethodSignature collectMethod() { + collectReturnOrThrows(); + + return new MethodSignature(generics, params, returnType, throwables); + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/PoetTypeSignatureWriter.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/PoetTypeSignatureWriter.java new file mode 100644 index 0000000000..c4e920b3aa --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/PoetTypeSignatureWriter.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet.signature; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Objects; + +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeVariableName; +import com.squareup.javapoet.WildcardTypeName; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.TypePath; +import org.objectweb.asm.signature.SignatureVisitor; + +import net.fabricmc.mappingpoet.Signatures; + +/** + * A type signature to javapoet visitor. + * + *

A type signature is one at the usage of type, such as field and local var type. + * It does not include class or method signatures where new generics like {@code } + * can be defined.

+ */ +public final class PoetTypeSignatureWriter extends SignatureVisitor { + + private final ClassStaticContext context; + private final ArrayList params = new ArrayList<>(); + private /* NonNull */ TypeAnnotationBank storage; // mutable + private TypeName result; + // array + private PoetTypeSignatureWriter arrayChild; + // class type signature stuff + private TypeName currentType; // ClassName or ParameterizedTypeName + private String nestedClassName; + // single type argument + private char activeTypeArgumentKind; + private PoetTypeSignatureWriter activeTypeArgument; + + public PoetTypeSignatureWriter(TypeAnnotationBank storage, ClassStaticContext context) { + super(Opcodes.ASM9); + this.storage = storage; + this.context = context; + } + + public TypeName compute() { + // array cleanup. doesn't have visit end TwT + if (arrayChild != null) { + result = ArrayTypeName.of(arrayChild.compute()); + + result = annotate(result); + } + + return Objects.requireNonNull(result, "writer did not visit"); + } + + private T annotate(T input) { + return AnnotationAwareDescriptors.annotate(input, storage); + } + + private void annotateResult() { + result = annotate(result); + } + + // primitives + @Override + public void visitBaseType(char descriptor) { + result = Signatures.getPrimitive(descriptor); + annotateResult(); + } + + // T, E etc + @Override + public void visitTypeVariable(String name) { + result = TypeVariableName.get(name); + annotateResult(); + } + + @Override + public SignatureVisitor visitArrayType() { + return arrayChild = new PoetTypeSignatureWriter(this.storage.advance(TypePath.ARRAY_ELEMENT, 0), context); + // post cleanup, annotate in #getResult() + } + + // outer class, may have instance inner class. ends with visitEnd + @Override + public void visitClassType(String internalName) { + Map.Entry entry = AnnotationAwareDescriptors.annotateUpTo(internalName, storage, context); + + currentType = entry.getKey(); + storage = entry.getValue(); + // later collect annotations in #collectPreviousTypeArgumentsAndAnnotations + } + + // collect info onto this before we append inners + private void collectPreviousTypeArgumentsAndAnnotations() { + collectLastTypeArgument(); + + if (!params.isEmpty()) { + if (currentType instanceof ParameterizedTypeName) { + // top-level handled already + currentType = ((ParameterizedTypeName) currentType).nestedClass(nestedClassName, params); + } else { // assume ClassName + if (nestedClassName == null) { // top-level + currentType = ParameterizedTypeName.get((ClassName) currentType, params.toArray(new TypeName[0])); + } else { + currentType = ParameterizedTypeName.get(((ClassName) currentType).nestedClass(nestedClassName), params.toArray(new TypeName[0])); + } + } + + params.clear(); + nestedClassName = null; + } else if (nestedClassName != null) { + if (currentType instanceof ParameterizedTypeName) { + currentType = ((ParameterizedTypeName) currentType).nestedClass(nestedClassName); + } else { + currentType = ((ClassName) currentType).nestedClass(nestedClassName); + } + nestedClassName = null; + } + + currentType = annotate(currentType); + } + + @Override + public void visitInnerClassType(String name) { + collectPreviousTypeArgumentsAndAnnotations(); + // collect previous type arguments + nestedClassName = name; + storage = storage.advance(TypePath.INNER_TYPE, 0); + } + + private void collectLastTypeArgument() { + if (activeTypeArgument != null) { + TypeName hold = activeTypeArgument.compute(); + TypeName used = switch (activeTypeArgumentKind) { + case SignatureVisitor.EXTENDS -> WildcardTypeName.subtypeOf(hold); + case SignatureVisitor.SUPER -> WildcardTypeName.supertypeOf(hold); + case SignatureVisitor.INSTANCEOF -> hold; + default -> throw new IllegalStateException(String.format("Illegal type argument wildcard %s", activeTypeArgumentKind)); + }; + + used = AnnotationAwareDescriptors.annotate(used, storage.advance(TypePath.TYPE_ARGUMENT, params.size())); + params.add(used); + + activeTypeArgument = null; + activeTypeArgumentKind = 0; + } + } + + // wildcard ? like in List + @Override + public void visitTypeArgument() { + collectLastTypeArgument(); + + TypeName used = WildcardTypeName.subtypeOf(TypeName.OBJECT); + used = AnnotationAwareDescriptors.annotate(used, storage.advance(TypePath.TYPE_ARGUMENT, params.size())); + params.add(used); + } + + // (? extends/ ? super)? ClassType like in Consumer, + // Supplier + @Override + public SignatureVisitor visitTypeArgument(char wildcard) { + collectLastTypeArgument(); + + activeTypeArgumentKind = wildcard; + return activeTypeArgument = new PoetTypeSignatureWriter(storage.advance(TypePath.WILDCARD_BOUND, 0), context); + } + + @Override + public void visitEnd() { + collectPreviousTypeArgumentsAndAnnotations(); + // finalize result! + result = currentType; + currentType = null; + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/TypeAnnotationBank.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/TypeAnnotationBank.java new file mode 100644 index 0000000000..2a26517d47 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/TypeAnnotationBank.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet.signature; + +import java.util.Collections; +import java.util.List; + +import com.squareup.javapoet.AnnotationSpec; + +// recommended storing with a sorted array + index slicing + +/** + * The collection of type annotations on a specific type. Can be narrowed down through type path. + */ +public interface TypeAnnotationBank { + TypeAnnotationBank EMPTY = new TypeAnnotationBank() { + @Override + public TypeAnnotationBank advance(int step, int stepArgument) { + return this; + } + + @Override + public List getCurrentAnnotations() { + return Collections.emptyList(); + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + /** + * Make the scope of type annotations smaller. + * + * @param step see {@link org.objectweb.asm.TypePath#getStep(int)} + * @param stepArgument see {@link org.objectweb.asm.TypePath#getStepArgument(int)} + * @return the sliced type annotation storage + */ + TypeAnnotationBank advance(int step, int stepArgument); + + /** + * Accesses annotations applicable at current type location. + * + *

Do not modify the returned list!

+ * + * @return the current annotations to apply + */ + List getCurrentAnnotations(); + + /** + * Returns if there is no more annotations. Used to check for receiver + * declarations. + * + * @return whether there's no more annotations + */ + boolean isEmpty(); +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/TypeAnnotationMapping.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/TypeAnnotationMapping.java new file mode 100644 index 0000000000..340cd25e16 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/TypeAnnotationMapping.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet.signature; + +import org.objectweb.asm.TypeReference; + +/** + * The collection of type annotations from a bytecode structure that stores type annotations. + */ +public interface TypeAnnotationMapping { + + TypeAnnotationMapping EMPTY = reference -> TypeAnnotationBank.EMPTY; + + // implNote: TypeReference is not a pojo! No equals or hash! + TypeAnnotationBank getBank(TypeReference reference); +} diff --git a/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/TypeAnnotationStorage.java b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/TypeAnnotationStorage.java new file mode 100644 index 0000000000..c1322661e0 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/mappingpoet/signature/TypeAnnotationStorage.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2020 FabricMC + * + * 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 net.fabricmc.mappingpoet.signature; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import com.squareup.javapoet.AnnotationSpec; +import org.objectweb.asm.TypePath; +import org.objectweb.asm.TypeReference; +import org.objectweb.asm.tree.TypeAnnotationNode; + +import net.fabricmc.mappingpoet.FieldBuilder; + +public final class TypeAnnotationStorage implements TypeAnnotationMapping, TypeAnnotationBank { + + private final int[] targets; // target type and info, only exist in mapping version + private final String[] paths; + private final AnnotationSpec[] contents; + private final int startIndex; + private final int endIndex; + private final String currentPath; + + TypeAnnotationStorage(int startIndex, int endIndex, String currentPath, int[] targets, String[] paths, AnnotationSpec[] contents) { + this.targets = targets; + this.paths = paths; + this.contents = contents; + this.startIndex = startIndex; + this.endIndex = endIndex; + this.currentPath = currentPath; + } + + public static Builder builder() { + return new Builder(); + } + + static int comparePath(TypePath left, TypePath right) { + int len = Math.min(left.getLength(), right.getLength()); + for (int i = 0; i < len; i++) { + int leftStep = left.getStep(i); + int rightStep = right.getStep(i); + if (leftStep != rightStep) { + return Integer.compare(leftStep, rightStep); + } + + int leftStepArg = left.getStepArgument(i); + int rightStepArg = right.getStepArgument(i); + if (leftStepArg != rightStepArg) { + return Integer.compare(leftStepArg, rightStepArg); + } + } + + // shorter ones definitely go first! + return Integer.compare(left.getLength(), right.getLength()); + } + + @Override + public TypeAnnotationBank advance(int step, int stepArgument) { + if (currentPath == null) { + throw new IllegalStateException(); + } + + String suffix; + switch (step) { + case TypePath.ARRAY_ELEMENT: + suffix = "["; + break; + case TypePath.INNER_TYPE: + suffix = "."; + break; + case TypePath.WILDCARD_BOUND: + suffix = "*"; + break; + case TypePath.TYPE_ARGUMENT: + suffix = stepArgument + ";"; + break; + default: + throw new IllegalArgumentException(); + } + + String check = currentPath.concat(suffix); + + String hiCheck = check.substring(0, check.length() - 1).concat(Character.toString((char) (check.charAt(check.length() - 1) + 1))); + + + int low = Arrays.binarySearch(paths, startIndex, endIndex, check); + if (low < 0) { + low = -(low + 1); + } + + // exclusive hi + int hi = Arrays.binarySearch(paths, startIndex, endIndex, hiCheck); + if (hi < 0) { + hi = -(hi + 1); + } + + return new TypeAnnotationStorage(low, hi, check, null, paths, contents); + } + + @Override + public List getCurrentAnnotations() { + if (currentPath == null) { + throw new IllegalStateException(); + } + + int hi = Arrays.binarySearch(paths, startIndex, endIndex, currentPath + '\u0000'); + if (hi < 0) { + hi = -(hi + 1); + } + + return Arrays.asList(contents).subList(startIndex, hi); + } + + @Override + public boolean isEmpty() { + return startIndex >= endIndex; + } + + @Override + public TypeAnnotationBank getBank(TypeReference reference) { + if (targets == null) { + throw new IllegalStateException(); + } + + int target = reference.getValue(); + // inclusive low + int low = Arrays.binarySearch(targets, startIndex, endIndex, target); + if (low < 0) { + low = -(low + 1); + } + + // exclusive hi + int hi = Arrays.binarySearch(targets, startIndex, endIndex, target + 1); + if (hi < 0) { + hi = -(hi + 1); + } + + return new TypeAnnotationStorage(low, hi, "", null, paths, contents); + } + + public static final class Builder { + + final List entries = new ArrayList<>(); + + Builder() { + } + + public Builder add(int typeReference, String typePath, AnnotationSpec spec) { + entries.add(new Entry(typeReference, typePath, spec)); + return this; + } + + public Builder add(Iterable nodes) { + if (nodes == null) { + return this; // thanks asm + } + for (TypeAnnotationNode node : nodes) { + entries.add(new Entry(node.typeRef, node.typePath == null ? "" : node.typePath.toString(), FieldBuilder.parseAnnotation(node))); + } + return this; + } + + public TypeAnnotationMapping build() { + this.entries.sort(null); + int len = this.entries.size(); + + int[] targets = new int[len]; + String[] paths = new String[len]; + AnnotationSpec[] contents = new AnnotationSpec[len]; + + Iterator itr = this.entries.iterator(); + for (int i = 0; i < len; i++) { + Entry entry = itr.next(); + targets[i] = entry.target; + paths[i] = entry.path; + contents[i] = entry.content; + } + + return new TypeAnnotationStorage(0, len, null, targets, paths, contents); + } + + private static final class Entry implements Comparable { + final int target; + final String path; + final AnnotationSpec content; + + Entry(int target, String path, AnnotationSpec content) { + this.target = target; + this.path = path; + this.content = content; + } + + @Override + public int compareTo(Entry o) { + int c0 = Integer.compare(target, o.target); + if (c0 != 0) return c0; + return path.compareTo(o.path); + } + } + + } +}