diff --git a/Discord.Net.sln b/Discord.Net.sln
index 58bfcad86..600c1dd67 100644
--- a/Discord.Net.sln
+++ b/Discord.Net.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
-VisualStudioVersion = 15.0.26730.12
+VisualStudioVersion = 15.0.27130.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}"
EndProject
@@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\D
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{BBA8E7FB-C834-40DC-822F-B112CB7F0140}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -130,6 +132,18 @@ Global
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.Build.0 = Release|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.ActiveCfg = Release|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.Build.0 = Release|Any CPU
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x64.Build.0 = Debug|Any CPU
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x86.Build.0 = Debug|Any CPU
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.ActiveCfg = Release|Any CPU
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.Build.0 = Release|Any CPU
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.ActiveCfg = Release|Any CPU
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -141,6 +155,7 @@ Global
{688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E}
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012}
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
+ {BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495}
diff --git a/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj
new file mode 100644
index 000000000..94e788547
--- /dev/null
+++ b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj
@@ -0,0 +1,30 @@
+
+
+ 1.0.2
+ RogueException
+ discord;discordapp
+ https://github.com/RogueException/Discord.Net
+ http://opensource.org/licenses/MIT
+ git
+ git://github.com/RogueException/Discord.Net
+ Discord.Net.Analyzers
+ Discord.Analyzers
+ A Discord.Net extension adding support for design-time analysis of the API usage.
+ netstandard1.3
+
+
+ $(DefineConstants);FILESYSTEM;DEFAULTUDPCLIENT;DEFAULTWEBSOCKET
+
+
+ $(DefineConstants);FORMATSTR;UNIXTIME;MSTRYBUFFER;UDPDISPOSE
+
+
+ $(NoWarn);CS1573;CS1591
+ true
+ true
+
+
+
+
+
+
diff --git a/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs b/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs
new file mode 100644
index 000000000..9e6277b65
--- /dev/null
+++ b/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Discord.Commands;
+
+namespace Discord.Analyzers
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public sealed class GuildAccessAnalyzer : DiagnosticAnalyzer
+ {
+ private const string DiagnosticId = "DNET0001";
+ private const string Title = "Limit command to Guild contexts.";
+ private const string MessageFormat = "Command method '{0}' is accessing 'Context.Guild' but is not restricted to Guild contexts.";
+ private const string Description = "Accessing 'Context.Guild' in a command without limiting the command to run only in guilds.";
+ private const string Category = "Design";
+
+ private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression);
+ }
+
+ private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
+ {
+ // Bail out if the containing class doesn't derive from 'ModuleBase'
+ var classNode = context.Node.FirstAncestorOrSelf();
+ var classSymbol = context.SemanticModel.GetSymbolInfo(classNode).Symbol as INamedTypeSymbol;
+ if (!DerivesFromModuleBase(classSymbol))
+ return;
+
+ // Bail out if the containing method isn't marked with '[Command]'
+ var methodNode = context.Node.FirstAncestorOrSelf();
+ var methodSymbol = context.SemanticModel.GetSymbolInfo(methodNode).Symbol;
+ var methodAttributes = methodSymbol.GetAttributes();
+ if (!methodAttributes.Any(a => a.AttributeClass.Name == nameof(CommandAttribute)))
+ return;
+
+ // Are you geting a property named 'Guild'?
+ var memberAccessSymbol = context.SemanticModel.GetSymbolInfo(context.Node).Symbol as IMethodSymbol;
+ if (memberAccessSymbol.AssociatedSymbol.Name == "Guild") //I guess?
+ {
+ // Is the '[RequireContext]' attribute not applied to either the method or the class, or doesn't contain 'ContextType.Guild'?
+ var ctxAttribute = methodAttributes.SingleOrDefault(_attributeDataPredicate)
+ ?? classSymbol.GetAttributes().SingleOrDefault(_attributeDataPredicate);
+ if (ctxAttribute == null || ctxAttribute.ConstructorArguments.Any(arg => !arg.Value.Equals(ContextType.Guild)))
+ {
+ // Report the diagnostic
+ var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), methodSymbol.Name);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+
+ private static readonly Func _attributeDataPredicate = a => a.AttributeClass.Name == nameof(RequireContextAttribute);
+
+ private static readonly string _moduleBaseName = typeof(ModuleBase<>).Name;
+
+ private static bool DerivesFromModuleBase(INamedTypeSymbol symbol)
+ {
+ var bType = symbol.BaseType;
+ while (bType != null)
+ {
+ if (bType.MetadataName == _moduleBaseName)
+ return true;
+
+ bType = bType.BaseType;
+ }
+ return false;
+ }
+ }
+}