Skip to content

Commit

Permalink
New rule MA0151 Check the content of DebuggerDisplay attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
meziantou committed Dec 21, 2023
1 parent 9b940a4 commit f2c0789
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0148](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0148.md)|Usage|Use pattern matching instead of equality operators for discrete value|ℹ️||✔️|
|[MA0149](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0149.md)|Usage|Use pattern matching instead of inequality operators for discrete value|ℹ️||✔️|
|[MA0150](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0150.md)|Design|Do not call the default object.ToString explicitly|⚠️|||
|[MA0151](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0151.md)|Usage|DebuggerDisplay must contain valid members|⚠️|✔️||

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
|[MA0148](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0148.md)|Usage|Use pattern matching instead of equality operators for discrete value|<span title='Info'>ℹ️</span>||✔️|
|[MA0149](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0149.md)|Usage|Use pattern matching instead of inequality operators for discrete value|<span title='Info'>ℹ️</span>||✔️|
|[MA0150](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0150.md)|Design|Do not call the default object.ToString explicitly|<span title='Warning'>⚠️</span>|||
|[MA0151](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0151.md)|Usage|DebuggerDisplay must contain valid members|<span title='Warning'>⚠️</span>|✔️||

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -606,6 +607,9 @@ dotnet_diagnostic.MA0149.severity = none
# MA0150: Do not call the default object.ToString explicitly
dotnet_diagnostic.MA0150.severity = none
# MA0151: DebuggerDisplay must contain valid members
dotnet_diagnostic.MA0151.severity = warning
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1057,4 +1061,7 @@ dotnet_diagnostic.MA0149.severity = none
# MA0150: Do not call the default object.ToString explicitly
dotnet_diagnostic.MA0150.severity = none
# MA0151: DebuggerDisplay must contain valid members
dotnet_diagnostic.MA0151.severity = none
```
17 changes: 17 additions & 0 deletions docs/Rules/MA0151.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# MA0151 - DebuggerDisplay must contain valid members

Validate the content

````c#
[DebuggerDisplay("{Display}")] // compliant
public class Dummy1
{
public string Display { get; }
}

[DebuggerDisplay("{InvalidMember}")] // non-compliant as InvalidMember doesn't exist
public class Dummy2
{
public string Display { get; }
}
````
14 changes: 14 additions & 0 deletions src/Meziantou.Analyzer/Internals/ContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public static void ReportDiagnostic(this DiagnosticReporter context, DiagnosticD

context.ReportDiagnostic(descriptor, properties, operation, messageArgs);
}

public static void ReportDiagnostic(this DiagnosticReporter context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, IInvocationOperation operation, DiagnosticReportOptions options, params string?[]? messageArgs)
{
if (options.HasFlag(DiagnosticReportOptions.ReportOnMethodName) &&
Expand All @@ -70,4 +71,17 @@ public static void ReportDiagnostic(this DiagnosticReporter context, DiagnosticD

context.ReportDiagnostic(descriptor, properties, operation, messageArgs);
}

public static void ReportDiagnostic(this DiagnosticReporter context, DiagnosticDescriptor descriptor, AttributeData attribute, params string?[]? messageArgs)
{
ReportDiagnostic(context, descriptor, ImmutableDictionary<string, string?>.Empty, attribute, messageArgs);
}

public static void ReportDiagnostic(this DiagnosticReporter context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, AttributeData attribute, params string?[]? messageArgs)
{
if (attribute.ApplicationSyntaxReference is not null)
{
context.ReportDiagnostic(descriptor, properties, attribute.ApplicationSyntaxReference, messageArgs);
}
}
}
30 changes: 25 additions & 5 deletions src/Meziantou.Analyzer/Internals/ContextExtensions.g.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@





#nullable enable
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
Expand All @@ -13,7 +9,6 @@ namespace Meziantou.Analyzer;

internal static partial class ContextExtensions
{

public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, SyntaxToken syntaxToken, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, syntaxToken, messageArgs);

Expand Down Expand Up @@ -56,6 +51,11 @@ public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, Diag
public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, IOperation operation, params string?[] messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, properties, operation, messageArgs);

public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, AttributeData attribute, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, attribute, messageArgs);

public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, AttributeData attribute, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, properties, attribute, messageArgs);
public static void ReportDiagnostic(this SymbolAnalysisContext context, DiagnosticDescriptor descriptor, SyntaxToken syntaxToken, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, syntaxToken, messageArgs);

Expand Down Expand Up @@ -98,6 +98,11 @@ public static void ReportDiagnostic(this SymbolAnalysisContext context, Diagnost
public static void ReportDiagnostic(this SymbolAnalysisContext context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, IOperation operation, params string?[] messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, properties, operation, messageArgs);

public static void ReportDiagnostic(this SymbolAnalysisContext context, DiagnosticDescriptor descriptor, AttributeData attribute, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, attribute, messageArgs);

public static void ReportDiagnostic(this SymbolAnalysisContext context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, AttributeData attribute, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, properties, attribute, messageArgs);
public static void ReportDiagnostic(this OperationAnalysisContext context, DiagnosticDescriptor descriptor, SyntaxToken syntaxToken, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, syntaxToken, messageArgs);

Expand Down Expand Up @@ -140,6 +145,11 @@ public static void ReportDiagnostic(this OperationAnalysisContext context, Diagn
public static void ReportDiagnostic(this OperationAnalysisContext context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, IOperation operation, params string?[] messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, properties, operation, messageArgs);

public static void ReportDiagnostic(this OperationAnalysisContext context, DiagnosticDescriptor descriptor, AttributeData attribute, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, attribute, messageArgs);

public static void ReportDiagnostic(this OperationAnalysisContext context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, AttributeData attribute, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, properties, attribute, messageArgs);
public static void ReportDiagnostic(this OperationBlockAnalysisContext context, DiagnosticDescriptor descriptor, SyntaxToken syntaxToken, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, syntaxToken, messageArgs);

Expand Down Expand Up @@ -182,6 +192,11 @@ public static void ReportDiagnostic(this OperationBlockAnalysisContext context,
public static void ReportDiagnostic(this OperationBlockAnalysisContext context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, IOperation operation, params string?[] messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, properties, operation, messageArgs);

public static void ReportDiagnostic(this OperationBlockAnalysisContext context, DiagnosticDescriptor descriptor, AttributeData attribute, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, attribute, messageArgs);

public static void ReportDiagnostic(this OperationBlockAnalysisContext context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, AttributeData attribute, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, properties, attribute, messageArgs);
public static void ReportDiagnostic(this CompilationAnalysisContext context, DiagnosticDescriptor descriptor, SyntaxToken syntaxToken, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, syntaxToken, messageArgs);

Expand Down Expand Up @@ -224,4 +239,9 @@ public static void ReportDiagnostic(this CompilationAnalysisContext context, Dia
public static void ReportDiagnostic(this CompilationAnalysisContext context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, IOperation operation, params string?[] messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, properties, operation, messageArgs);

public static void ReportDiagnostic(this CompilationAnalysisContext context, DiagnosticDescriptor descriptor, AttributeData attribute, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, attribute, messageArgs);

public static void ReportDiagnostic(this CompilationAnalysisContext context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, AttributeData attribute, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, properties, attribute, messageArgs);
}
6 changes: 6 additions & 0 deletions src/Meziantou.Analyzer/Internals/ContextExtensions.tt
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,11 @@ internal static partial class ContextExtensions

public static void ReportDiagnostic(this <#= type #> context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, IOperation operation, params string?[] messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, properties, operation, messageArgs);

public static void ReportDiagnostic(this <#= type #> context, DiagnosticDescriptor descriptor, AttributeData attribute, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, attribute, messageArgs);

public static void ReportDiagnostic(this <#= type #> context, DiagnosticDescriptor descriptor, ImmutableDictionary<string, string?>? properties, AttributeData attribute, params string?[]? messageArgs)
=> ReportDiagnostic(new DiagnosticReporter(context), descriptor, properties, attribute, messageArgs);
<# } #>
}
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ internal static class RuleIdentifiers
public const string UsePatternMatchingForEqualityComparison = "MA0148";
public const string UsePatternMatchingForInequalityComparison = "MA0149";
public const string DoNotUseToStringIfObject = "MA0150";
public const string DebuggerDisplayAttributeShouldContainValidExpressions = "MA0151";

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

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DebuggerDisplayAttributeShouldContainValidExpressionsAnalyzer : DiagnosticAnalyzer
{
private static readonly char[] MemberSeparators = [',', '(', '.', '['];

private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.DebuggerDisplayAttributeShouldContainValidExpressions,
title: "DebuggerDisplay must contain valid members",
messageFormat: "Member '{0}' does not exist",
RuleCategories.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.DebuggerDisplayAttributeShouldContainValidExpressions));

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

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

context.RegisterCompilationStartAction(context =>
{
var attributeSymbol = context.Compilation.GetBestTypeByMetadataName("System.Diagnostics.DebuggerDisplayAttribute");
if (attributeSymbol is null)
return;

context.RegisterSymbolAction(context => AnalyzeNamedType(context, attributeSymbol), SymbolKind.NamedType);
});
}

private static void AnalyzeNamedType(SymbolAnalysisContext context, INamedTypeSymbol attributeSymbol)
{
var symbol = (INamedTypeSymbol)context.Symbol;
foreach (var attribute in symbol.GetAttributes())
{
if (!attribute.AttributeClass.IsEqualTo(attributeSymbol))
continue;

if (attribute.ConstructorArguments is [{ Kind: TypedConstantKind.Primitive, IsNull: false, Type.SpecialType: SpecialType.System_String, Value: string value }, ..])
{
var members = ParseMembers(value.AsSpan());
if (members is not null)
{
foreach (var member in members)
{
if (!MemberExists(symbol, member))
{
context.ReportDiagnostic(Rule, attribute, member);
return;
}
}
}
}
}

static bool MemberExists(INamedTypeSymbol? symbol, string name)
{
while (symbol is not null)
{
if (!symbol.GetMembers(name).IsEmpty)
return true;

symbol = symbol.BaseType;
}

return false;
}

static List<string>? ParseMembers(ReadOnlySpan<char> value)
{
List<string>? result = null;

while (!value.IsEmpty)
{
var startIndex = value.IndexOf('{');
if (startIndex < 0)
break;

value = value[(startIndex + 1)..];
var endIndex = value.IndexOf('}');
if (endIndex < 0)
break;

var member = value[..endIndex];


static string GetMemberName(ReadOnlySpan<char> member)
{
var index = member.IndexOfAny(MemberSeparators);
if (index < 0)
return member.ToString();

return member[..index].ToString();
}

result ??= [];
result.Add(GetMemberName(member));

value = value[(endIndex + 1)..];
}

return result;
}
}
}
Loading

0 comments on commit f2c0789

Please sign in to comment.