Skip to content

Commit

Permalink
Add MA0052
Browse files Browse the repository at this point in the history
  • Loading branch information
meziantou committed May 1, 2019
1 parent 91b158e commit a9e8e01
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 64 deletions.
42 changes: 34 additions & 8 deletions src/Meziantou.Analyzer/Internals/ContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Collections.Immutable;
using System;
using System.Collections.Immutable;
using System.Linq;
using Meziantou.Analyzer.Configurations;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
Expand All @@ -18,6 +20,30 @@ public static bool IsEnabled(this AnalyzerOptions options, DiagnosticDescriptor
return true;
}

public static DiagnosticSeverity? GetSeverity(this AnalyzerOptions options, DiagnosticDescriptor descriptor, string filePath)
{
if (options.TryGetConfigurationValue(filePath, "meziantou." + descriptor.Id + ".severity", out var value))
{
if (Enum.TryParse<DiagnosticSeverity>(value, out var result))
return result;
}

return null;
}

private static Diagnostic CreateDiagnostic(AnalyzerOptions options, DiagnosticDescriptor descriptor, Location location, ImmutableDictionary<string, string> properties, params string[] messageArgs)
{
var severity = GetSeverity(options, descriptor, location.SourceTree.FilePath);
if (severity == null)
{
return Diagnostic.Create(descriptor, location, properties, messageArgs);
}
else
{
return Diagnostic.Create(descriptor, location, severity.Value, Enumerable.Empty<Location>(), properties, messageArgs);
}
}

public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, SyntaxToken syntaxToken, params string[] messageArgs)
{
ReportDiagnostic(context, descriptor, ImmutableDictionary<string, string>.Empty, syntaxToken, messageArgs);
Expand All @@ -27,7 +53,7 @@ public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, Diag
{
if (IsEnabled(context.Options, descriptor, syntaxToken.SyntaxTree.FilePath))
{
context.ReportDiagnostic(Diagnostic.Create(descriptor, syntaxToken.GetLocation(), properties, messageArgs));
context.ReportDiagnostic(CreateDiagnostic(context.Options, descriptor, syntaxToken.GetLocation(), properties, messageArgs));
}
}

Expand All @@ -40,7 +66,7 @@ public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, Diag
{
if (IsEnabled(context.Options, descriptor, syntaxNode.SyntaxTree.FilePath))
{
context.ReportDiagnostic(Diagnostic.Create(descriptor, syntaxNode.GetLocation(), properties, messageArgs));
context.ReportDiagnostic(CreateDiagnostic(context.Options, descriptor, syntaxNode.GetLocation(), properties, messageArgs));
}
}

Expand All @@ -55,7 +81,7 @@ public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, Diag
{
if (IsEnabled(context.Options, descriptor, location.SourceTree.FilePath))
{
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, properties, messageArgs));
context.ReportDiagnostic(CreateDiagnostic(context.Options, descriptor, location, properties, messageArgs));
}
}
}
Expand All @@ -82,7 +108,7 @@ public static void ReportDiagnostic(this SymbolAnalysisContext context, Diagnost
{
if (IsEnabled(context.Options, descriptor, location.SourceTree.FilePath))
{
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, properties, messageArgs));
context.ReportDiagnostic(CreateDiagnostic(context.Options, descriptor, location, properties, messageArgs));
}
}

Expand All @@ -95,7 +121,7 @@ public static void ReportDiagnostic(this OperationAnalysisContext context, Diagn
{
if (IsEnabled(context.Options, descriptor, operation.Syntax.SyntaxTree.FilePath))
{
context.ReportDiagnostic(Diagnostic.Create(descriptor, operation.Syntax.GetLocation(), properties, messageArgs));
context.ReportDiagnostic(CreateDiagnostic(context.Options, descriptor, operation.Syntax.GetLocation(), properties, messageArgs));
}
}

Expand All @@ -110,7 +136,7 @@ public static void ReportDiagnostic(this CompilationAnalysisContext context, Dia
{
if (IsEnabled(context.Options, descriptor, location.SourceTree.FilePath))
{
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, properties, messageArgs));
context.ReportDiagnostic(CreateDiagnostic(context.Options, descriptor, location, properties, messageArgs));
}
}
}
Expand All @@ -126,7 +152,7 @@ public static void ReportDiagnostic(this OperationBlockAnalysisContext context,
{
if (IsEnabled(context.Options, descriptor, location.SourceTree.FilePath))
{
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, properties, messageArgs));
context.ReportDiagnostic(CreateDiagnostic(context.Options, descriptor, location, properties, messageArgs));
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ internal static class RuleIdentifiers
public const string TypeNameMustNotMatchNamespace = "MA0049";
public const string ValidateArgumentsCorrectly = "MA0050";
public const string MethodShouldNotBeTooLong = "MA0051";
public const string ReplaceEnumToStringWithNameof = "MA0052";

public static string GetHelpUri(string idenfifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Rules
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ReplaceEnumToStringWithNameofAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor s_rule = new DiagnosticDescriptor(
RuleIdentifiers.ReplaceEnumToStringWithNameof,
title: "Replace with nameof",
messageFormat: "Replace with nameof",
RuleCategories.Performance,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.ReplaceEnumToStringWithNameof));


public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(s_rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterOperationAction(Analyze, OperationKind.Invocation);
}

private static void Analyze(OperationAnalysisContext context)
{
var operation = (IInvocationOperation)context.Operation;
if (operation.TargetMethod.Name != nameof(object.ToString))
return;

if (!operation.TargetMethod.ContainingType.IsEqualTo(context.Compilation.GetSpecialType(SpecialType.System_Enum)))
return;

var expression = operation.Children.First() as IMemberReferenceOperation;
if (expression == null)
return;

if (expression.Member.ContainingType.EnumUnderlyingType == null)
return;

context.ReportDiagnostic(s_rule, operation);
}
}
}
47 changes: 47 additions & 0 deletions src/Meziantou.Analyzer/Rules/ReplaceEnumToStringWithNameofFixer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Rules
{
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public class ReplaceEnumToStringWithNameofFixer : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.ReplaceEnumToStringWithNameof);

public override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var nodeToFix = root.FindNode(context.Span, getInnermostNodeForTie: true);
if (nodeToFix == null)
return;

var title = "Use nameof";
context.RegisterCodeFix(CodeAction.Create(title, ct => UseNameof(context.Document, nodeToFix, ct), equivalenceKey: title), context.Diagnostics);
}

private static async Task<Document> UseNameof(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
var generator = editor.Generator;
var operation = (IInvocationOperation)editor.SemanticModel.GetOperation(nodeToFix, cancellationToken);

var newExpression = generator.NameOfExpression(operation.Children.First().Syntax);

editor.ReplaceNode(nodeToFix, newExpression);
return editor.GetChangedDocument();
}
}
}
49 changes: 2 additions & 47 deletions tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.Validation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public async Task ValidateAsync()

if (ExpectedFixedCode != null)
{
await VerifyFix(DiagnosticAnalyzer, CodeFixProvider, ExpectedFixedCode, CodeFixIndex, allowNewCompilerDiagnostics: AllowNewCompilerDiagnostics).ConfigureAwait(false);
await VerifyFix(DiagnosticAnalyzer, CodeFixProvider, ExpectedFixedCode, CodeFixIndex).ConfigureAwait(false);
}
}

Expand Down Expand Up @@ -343,7 +343,7 @@ private async static Task<IEnumerable<Diagnostic>> GetCompilerDiagnostics(Docume
return semanticModel.GetDiagnostics();
}

private async Task VerifyFix(DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics)
private async Task VerifyFix(DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string newSource, int? codeFixIndex)
{
var document = CreateProject().Documents.First();
var analyzerDiagnostics = await GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }, compileSolution: false).ConfigureAwait(false);
Expand Down Expand Up @@ -374,29 +374,6 @@ private async Task VerifyFix(DiagnosticAnalyzer analyzer, CodeFixProvider codeFi

document = await ApplyFix(document, actions[0]).ConfigureAwait(false);
analyzerDiagnostics = await GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }, compileSolution: false).ConfigureAwait(false);

var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, await GetCompilerDiagnostics(document).ConfigureAwait(false));

//check if applying the code fix introduced any new compiler diagnostics
if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any())
{
// Format and get the compiler diagnostics again so that the locations make sense in the output
var syntaxRoot = await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var formattedDocument = Formatter.Format(syntaxRoot, Formatter.Annotation, document.Project.Solution.Workspace, cancellationToken: context.CancellationToken);
document = document.WithSyntaxRoot(formattedDocument);
newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, await GetCompilerDiagnostics(document).ConfigureAwait(false));

Assert.IsTrue(false,
string.Format("Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n",
string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())),
(await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false)).ToFullString()));
}

//check if there are analyzer diagnostics left after the code fix
if (analyzerDiagnostics.Length == 0)
{
break;
}
}

//after applying all of the code fixes, compare the resulting string to the inputted one
Expand All @@ -411,28 +388,6 @@ private static async Task<Document> ApplyFix(Document document, CodeAction codeA
return solution.GetDocument(document.Id);
}

private static IEnumerable<Diagnostic> GetNewDiagnostics(IEnumerable<Diagnostic> diagnostics, IEnumerable<Diagnostic> newDiagnostics)
{
var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();

int oldIndex = 0;
int newIndex = 0;

while (newIndex < newArray.Length)
{
if (oldIndex < oldArray.Length && string.Equals(oldArray[oldIndex].Id, newArray[newIndex].Id, StringComparison.Ordinal))
{
++oldIndex;
++newIndex;
}
else
{
yield return newArray[newIndex++];
}
}
}

private static async Task<string> GetStringFromDocument(Document document)
{
var simplifiedDoc = await Simplifier.ReduceAsync(document, Simplifier.Annotation).ConfigureAwait(false);
Expand Down
7 changes: 0 additions & 7 deletions tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public ProjectBuilder()
public int? CodeFixIndex { get; private set; }
public string DefaultAnalyzerId { get; set; }
public string DefaultAnalyzerMessage { get; set; }
public bool AllowNewCompilerDiagnostics { get; set; }

private ProjectBuilder AddApiReference(string name)
{
Expand Down Expand Up @@ -195,11 +194,5 @@ public ProjectBuilder ShouldFixCodeWith(int? index, string codeFix)
CodeFixIndex = index;
return this;
}

public ProjectBuilder CodeFixAllowNewCompilerDiagnostics()
{
AllowNewCompilerDiagnostics = true;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ShouldReportDiagnostic(line: 7, column: 54)
.ShouldFixCodeWith(CodeFix)
.CodeFixAllowNewCompilerDiagnostics() // TODO remove
.ValidateAsync();
}

Expand Down Expand Up @@ -71,7 +70,6 @@ await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ShouldReportDiagnostic(line: 6, column: 48)
.ShouldFixCodeWith(CodeFix)
.CodeFixAllowNewCompilerDiagnostics() // TODO remove
.ValidateAsync();
}
}
Expand Down

0 comments on commit a9e8e01

Please sign in to comment.