using System; using System.Linq; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; using Discord.Commands.Builders; namespace Discord.Commands { internal static class ModuleClassBuilder { private static readonly TypeInfo _moduleTypeInfo = typeof(ModuleBase).GetTypeInfo(); public static IEnumerable Search(Assembly assembly) { foreach (var type in assembly.ExportedTypes) { var typeInfo = type.GetTypeInfo(); if (IsValidModuleDefinition(typeInfo) && !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) { yield return typeInfo; } } } public static Dictionary Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); public static Dictionary Build(IEnumerable validTypes, CommandService service) { if (!validTypes.Any()) throw new InvalidOperationException("Could not find any valid modules from the given selection"); var topLevelGroups = validTypes.Where(x => x.DeclaringType == null); var subGroups = validTypes.Intersect(topLevelGroups); var builtTypes = new List(); var result = new Dictionary(); foreach (var typeInfo in topLevelGroups) { // TODO: This shouldn't be the case; may be safe to remove? if (result.ContainsKey(typeInfo.AsType())) continue; var module = new ModuleBuilder(service, null); BuildModule(module, typeInfo, service); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); result[typeInfo.AsType()] = module.Build(service); } return result; } private static void BuildSubTypes(ModuleBuilder builder, IEnumerable subTypes, List builtTypes, CommandService service) { foreach (var typeInfo in subTypes) { if (!IsValidModuleDefinition(typeInfo)) continue; if (builtTypes.Contains(typeInfo)) continue; builder.AddModule((module) => { BuildModule(module, typeInfo, service); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); }); builtTypes.Add(typeInfo); } } private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service) { var attributes = typeInfo.GetCustomAttributes(); foreach (var attribute in attributes) { // TODO: C#7 type switch if (attribute is NameAttribute) builder.Name = (attribute as NameAttribute).Text; else if (attribute is SummaryAttribute) builder.Summary = (attribute as SummaryAttribute).Text; else if (attribute is RemarksAttribute) builder.Remarks = (attribute as RemarksAttribute).Text; else if (attribute is AliasAttribute) builder.AddAlias((attribute as AliasAttribute).Aliases); else if (attribute is GroupAttribute) { var groupAttr = attribute as GroupAttribute; builder.Name = builder.Name ?? groupAttr.Prefix; builder.AddAlias(groupAttr.Prefix); } else if (attribute is PreconditionAttribute) builder.AddPrecondition(attribute as PreconditionAttribute); } if (builder.Name == null) builder.Name = typeInfo.Name; var validCommands = typeInfo.DeclaredMethods.Where(x => IsValidCommandDefinition(x)); foreach (var method in validCommands) { builder.AddCommand((command) => { BuildCommand(command, typeInfo, method, service); }); } } private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service) { var attributes = method.GetCustomAttributes(); foreach (var attribute in attributes) { // TODO: C#7 type switch if (attribute is CommandAttribute) { var cmdAttr = attribute as CommandAttribute; builder.AddAliases(cmdAttr.Text); builder.RunMode = cmdAttr.RunMode; builder.Name = builder.Name ?? cmdAttr.Text; } else if (attribute is NameAttribute) builder.Name = (attribute as NameAttribute).Text; else if (attribute is PriorityAttribute) builder.Priority = (attribute as PriorityAttribute).Priority; else if (attribute is SummaryAttribute) builder.Summary = (attribute as SummaryAttribute).Text; else if (attribute is RemarksAttribute) builder.Remarks = (attribute as RemarksAttribute).Text; else if (attribute is AliasAttribute) builder.AddAliases((attribute as AliasAttribute).Aliases); else if (attribute is PreconditionAttribute) builder.AddPrecondition(attribute as PreconditionAttribute); } if (builder.Name == null) builder.Name = method.Name; var parameters = method.GetParameters(); int pos = 0, count = parameters.Length; foreach (var paramInfo in parameters) { builder.AddParameter((parameter) => { BuildParameter(parameter, paramInfo, pos++, count, service); }); } var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); builder.Callback = (ctx, args, map) => { var instance = createInstance(map); instance.Context = ctx; try { return method.Invoke(instance, args) as Task ?? Task.CompletedTask; } finally{ (instance as IDisposable)?.Dispose(); } }; } private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) { var attributes = paramInfo.GetCustomAttributes(); var paramType = paramInfo.ParameterType; builder.Name = paramInfo.Name; builder.IsOptional = paramInfo.IsOptional; builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null; foreach (var attribute in attributes) { // TODO: C#7 type switch if (attribute is SummaryAttribute) builder.Summary = (attribute as SummaryAttribute).Text; else if (attribute is OverrideTypeReaderAttribute) { builder.TypeReader = GetTypeReader(service, paramType, (attribute as OverrideTypeReaderAttribute).TypeReader); } else if (attribute is ParameterPreconditionAttribute) builder.AddPrecondition(attribute as ParameterPreconditionAttribute); else if (attribute is ParamArrayAttribute) { builder.IsMultiple = true; paramType = paramType.GetElementType(); } else if (attribute is RemainderAttribute) { if (position != count-1) throw new InvalidOperationException("Remainder parameters must be the last parameter in a command."); builder.IsRemainder = true; } } if (builder.TypeReader == null) { var readers = service.GetTypeReaders(paramType); var reader = readers?.FirstOrDefault(); if (reader == null) { var paramTypeInfo = paramType.GetTypeInfo(); if (paramTypeInfo.IsEnum) { reader = EnumTypeReader.GetReader(paramType); service.AddTypeReader(paramType, reader); } else { throw new InvalidOperationException($"{paramType.FullName} is not supported as a command parameter, are you missing a TypeReader?"); } } builder.ParameterType = paramType; builder.TypeReader = reader; } } private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType) { var readers = service.GetTypeReaders(paramType); if (readers != null) { var reader = readers.FirstOrDefault(x => x.GetType() == typeReaderType); if (reader != default(TypeReader)) { return reader; } } //could not find any registered type reader: try to create one var typeReader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, DependencyMap.Empty); service.AddTypeReader(paramType, typeReader); return typeReader; } private static bool IsValidModuleDefinition(TypeInfo typeInfo) { return _moduleTypeInfo.IsAssignableFrom(typeInfo) && !typeInfo.IsAbstract; } private static bool IsValidCommandDefinition(MethodInfo methodInfo) { return methodInfo.IsDefined(typeof(CommandAttribute)) && methodInfo.ReturnType == typeof(Task) && !methodInfo.IsStatic && !methodInfo.IsGenericMethod; } } }