From 67cba900d94844b9feb9d5521ce307d88e968cce Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Thu, 7 Dec 2017 18:16:12 +0100 Subject: [PATCH] Start on API analyzers --- Discord.Net.sln | 17 +++- .../Discord.Net.Analyzers.csproj | 30 +++++++ .../GuildAccessAnalyzer.cs | 78 +++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj create mode 100644 src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs 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; + } + } +}