package scotch.compiler; import static java.util.Arrays.asList; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static me.qmx.jitescript.util.CodegenUtils.p; import static scotch.compiler.text.TextUtil.quote; import static scotch.symbol.Operator.operator; import static scotch.symbol.Symbol.qualified; import static scotch.symbol.Symbol.symbol; import static scotch.symbol.Value.Fixity.NONE; import static scotch.symbol.descriptor.DataFieldDescriptor.field; import static scotch.symbol.descriptor.TypeClassDescriptor.typeClass; import static scotch.symbol.descriptor.TypeInstanceDescriptor.typeInstance; import static scotch.util.Pair.pair; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import com.google.common.collect.ImmutableSet; import lombok.Getter; import scotch.symbol.DataConstructor; import scotch.symbol.DataField; import scotch.symbol.DataFieldType; import scotch.symbol.DataType; import scotch.symbol.InstanceGetter; import scotch.symbol.Member; import scotch.symbol.MethodSignature; import scotch.symbol.Module; import scotch.symbol.ReExportModule; import scotch.symbol.Symbol; import scotch.symbol.SymbolEntry; import scotch.symbol.SymbolEntry.ImmutableEntryBuilder; import scotch.symbol.TypeClass; import scotch.symbol.TypeInstance; import scotch.symbol.TypeParameters; import scotch.symbol.Value; import scotch.symbol.ValueType; import scotch.symbol.descriptor.DataConstructorDescriptor; import scotch.symbol.descriptor.DataTypeDescriptor; import scotch.symbol.descriptor.TypeInstanceDescriptor; import scotch.symbol.exception.IncompleteDataTypeError; import scotch.symbol.exception.IncompleteTypeInstanceError; import scotch.symbol.exception.InvalidMethodSignatureError; import scotch.symbol.exception.SymbolResolutionError; import scotch.symbol.type.TypeDescriptor; import scotch.symbol.type.TypeDescriptors; public class ModuleScanner { private final String moduleName; private final List<Class<?>> classes; private final Map<Symbol, ImmutableEntryBuilder> builders; private final Set<TypeInstanceDescriptor> typeInstances; private final Map<String, String> reExports; public ModuleScanner(String moduleName, List<Class<?>> classes) { this.moduleName = moduleName; this.classes = classes; this.builders = new HashMap<>(); this.typeInstances = new HashSet<>(); this.reExports = new LinkedHashMap<>(); } public ScanResult scan() { classes.forEach(this::processModules); classes.forEach(this::processDataTypes); classes.forEach(this::processDataConstructors); classes.forEach(clazz -> { processTypeClasses(clazz); processTypeInstances(clazz); processValues(clazz); }); return new ScanResult( builders.values().stream().map(ImmutableEntryBuilder::build).collect(toSet()), typeInstances, reExports ); } private List<Symbol> computeMembers(Class<?> clazz) { return stream(clazz.getDeclaredMethods()) .filter(method -> method.isAnnotationPresent(Member.class)) .map(method -> method.getAnnotation(Member.class)) .map(Member::value) .map(member -> qualified(moduleName, member)) .collect(toList()); } private List<TypeDescriptor> computeParameters(TypeClass typeClass) { return stream(typeClass.parameters()) .map(parameter -> TypeDescriptors.var(parameter.name(), asList(parameter.constraints()))) .collect(toList()); } private Optional<Method> findMethod(Class<?> clazz, Class<? extends Annotation> annotation) { return stream(clazz.getDeclaredMethods()) .filter(method -> method.isAnnotationPresent(annotation)) .findFirst(); } private ImmutableEntryBuilder getBuilder(String memberName) { return builders.computeIfAbsent(qualify(memberName), SymbolEntry::immutableEntry); } private ImmutableEntryBuilder getBuilder(Symbol memberSymbol) { return builders.computeIfAbsent(memberSymbol, SymbolEntry::immutableEntry); } private IncompleteDataTypeError incompleteDataType(Class<?> clazz, Class<? extends Annotation> missingAnnotation) { return new IncompleteDataTypeError("Data type definition for class " + quote(clazz) + " in module " + quote(moduleName) + " is incomplete: missing method annotated with " + pp(missingAnnotation)); } private IncompleteTypeInstanceError incompleteTypeInstance(TypeInstance typeInstance, Class<? extends Annotation> missingAnnotation) { return new IncompleteTypeInstanceError("Type instance definition for class " + quote(typeInstance.typeClass()) + " in module " + quote(moduleName) + " is incomplete" + ": missing method annotated with " + pp(missingAnnotation)); } @SuppressWarnings("unchecked") private <T> T invoke(Method method) { try { method.setAccessible(true); return (T) method.invoke(null); } catch (ReflectiveOperationException exception) { throw new SymbolResolutionError(exception); } } private String pp(Class<?> clazz) { return clazz.getCanonicalName(); } private String pp(Method method) { return pp(method.getDeclaringClass()) + "#" + method.getName(); } private void processDataConstructors(Class<?> clazz) { Optional.ofNullable(clazz.getAnnotation(DataConstructor.class)).ifPresent(annotation -> { Symbol constructor = qualify(annotation.memberName()); Symbol dataType = qualify(annotation.dataType()); DataConstructorDescriptor.Builder builder = getBuilder(constructor).dataConstructor(annotation.ordinal(), dataType, clazz.getName().replace('.', '/')); Map<String, TypeDescriptor> fieldTypes = stream(clazz.getDeclaredMethods()) .filter(method -> method.isAnnotationPresent(DataFieldType.class)) .map(method -> pair(method.getAnnotation(DataFieldType.class).forMember(), method)) .collect( HashMap::new, (map, pair) -> pair.into((name, method) -> { map.put(name, invoke(method)); return map; }), HashMap::putAll ); stream(clazz.getDeclaredMethods()) .filter(method -> method.isAnnotationPresent(DataField.class)) .map(method -> pair(method, method.getAnnotation(DataField.class))) .forEach(pair -> pair.into((method, field) -> builder.addField(field( field.ordinal(), field.memberName(), method.getName(), Optional.ofNullable(fieldTypes.get(field.memberName())) .orElseThrow(() -> incompleteDataType(clazz, DataFieldType.class)) )))); getBuilder(dataType).dataType().addConstructor(builder.build()); }); } private void processDataTypes(Class<?> clazz) { Optional.ofNullable(clazz.getAnnotation(DataType.class)).ifPresent(annotation -> { Symbol symbol = qualify(annotation.memberName()); DataTypeDescriptor.Builder builder = getBuilder(symbol).dataType(); Method parametersGetter = findMethod(clazz, TypeParameters.class).orElseThrow(() -> incompleteDataType(clazz, TypeParameters.class)); validateParametersGetter(parametersGetter); List<TypeDescriptor> parametersList = invoke(parametersGetter); for (int i = 0; i < annotation.parameters().length; i++) { builder.addParameter(parametersList.get(i)); } builder.withClassName(p(clazz)); }); } private void processModules(Class<?> clazz) { if (!reExports.isEmpty()) { throw new SymbolResolutionError( "Multiple classes in module " + quote(moduleName) + " found with annotation @Module:" + " duplicate class is " + quote(clazz.getName()) ); } else if (clazz.isAnnotationPresent(Module.class)) { for (ReExportModule module : clazz.getAnnotation(Module.class).reExports()) { stream(module.members()).forEach(member -> reExports.put(member.memberName(), module.moduleName())); } } } private void processTypeClasses(Class<?> clazz) { Optional.ofNullable(clazz.getAnnotation(TypeClass.class)).ifPresent(typeClass -> { ImmutableEntryBuilder builder = getBuilder(typeClass.memberName()); Symbol symbol = qualify(typeClass.memberName()); List<Symbol> members = computeMembers(clazz); builder.withTypeClass(typeClass(symbol, computeParameters(typeClass), members)); members.forEach(member -> getBuilder(member).withMemberOf(symbol)); }); } private void processTypeInstances(Class<?> clazz) { Optional.ofNullable(clazz.getAnnotation(TypeInstance.class)).ifPresent(typeInstance -> { Method parametersGetter = findMethod(clazz, TypeParameters.class).orElseThrow(() -> incompleteTypeInstance(typeInstance, TypeParameters.class)); Method instanceGetter = findMethod(clazz, InstanceGetter.class).orElseThrow(() -> incompleteTypeInstance(typeInstance, InstanceGetter.class)); validateParametersGetter(parametersGetter); typeInstances.add(typeInstance( moduleName, symbol(typeInstance.typeClass()), invoke(parametersGetter), MethodSignature.fromMethod(instanceGetter) )); }); } private void processValues(Class<?> clazz) { stream(clazz.getDeclaredMethods()).forEach(method -> { Optional.ofNullable(method.getAnnotation(Value.class)).ifPresent(value -> { ImmutableEntryBuilder builder = getBuilder(value.memberName()); builder.withValueMethod(MethodSignature.fromMethod(method)); if (value.fixity() != NONE && value.precedence() != -1) { builder.withOperator(operator(value.fixity(), value.precedence())); } }); Optional.ofNullable(method.getAnnotation(ValueType.class)).ifPresent(valueType -> { ImmutableEntryBuilder builder = getBuilder(valueType.forMember()); if (TypeDescriptor.class.isAssignableFrom(method.getReturnType())) { builder.withValueType(invoke(method)); } else { throw new InvalidMethodSignatureError("Method " + pp(method) + " annotated by " + pp(ValueType.class) + " does not return " + pp(TypeDescriptor.class)); } }); }); } private Symbol qualify(String memberName) { return qualified(moduleName, memberName); } private void validateParametersGetter(Method parametersGetter) { ParameterizedType returnType = (ParameterizedType) parametersGetter.getGenericReturnType(); if (!List.class.isAssignableFrom((Class<?>) returnType.getRawType()) || !TypeDescriptor.class.isAssignableFrom((Class<?>) returnType.getActualTypeArguments()[0])) { throw new InvalidMethodSignatureError("Method " + pp(parametersGetter) + " annotated by " + pp(TypeParameters.class) + " does not return " + pp(List.class) + "<" + pp(TypeDescriptor.class) + ">"); } else if (parametersGetter.getParameterCount() != 0) { throw new InvalidMethodSignatureError("Method " + pp(parametersGetter) + " annotated by " + pp(TypeParameters.class) + " should not accept arguments"); } } public static final class ScanResult { @Getter private final Set<SymbolEntry> entries; @Getter private final Set<TypeInstanceDescriptor> instances; private final Map<String, String> reExports; public ScanResult(Set<SymbolEntry> entries, Set<TypeInstanceDescriptor> instances, Map<String, String> reExports) { this.entries = ImmutableSet.copyOf(entries); this.instances = ImmutableSet.copyOf(instances); this.reExports = new LinkedHashMap<>(reExports); } public Map<String, String> getReExports() { return new LinkedHashMap<>(reExports); } } }