Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion src/Meziantou.Analyzer/Internals/SymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -85,6 +85,24 @@ public static IEnumerable<ISymbol> GetAllMembers(this ITypeSymbol? symbol)
}
}

public static IEnumerable<ISymbol> GetAllMembers(this INamespaceOrTypeSymbol? symbol)
{
while (symbol is not null)
{
foreach (var member in symbol.GetMembers())
yield return member;

if (symbol is ITypeSymbol typeSymbol)
{
symbol = typeSymbol.BaseType;
}
else
{
yield break;
}
}
}

public static IEnumerable<ISymbol> GetAllMembers(this ITypeSymbol? symbol, string name)
{
while (symbol is not null)
Expand All @@ -96,6 +114,24 @@ public static IEnumerable<ISymbol> GetAllMembers(this ITypeSymbol? symbol, strin
}
}

public static IEnumerable<ISymbol> GetAllMembers(this INamespaceOrTypeSymbol? symbol, string name)
{
while (symbol is not null)
{
foreach (var member in symbol.GetMembers(name))
yield return member;

if (symbol is ITypeSymbol typeSymbol)
{
symbol = typeSymbol.BaseType;
}
else
{
yield break;
}
}
}

public static bool IsTopLevelStatement(this ISymbol symbol, CancellationToken cancellationToken)
{
if (symbol.DeclaringSyntaxReferences.Length == 0)
Expand Down Expand Up @@ -142,6 +178,7 @@ public static bool IsTopLevelStatementsEntryPointType([NotNullWhen(true)] this I
IFieldSymbol field => field.Type,
IPropertySymbol { GetMethod: not null } property => property.Type,
ILocalSymbol local => local.Type,
IMethodSymbol method => method.ReturnType,
_ => null,
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,42 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.SymbolStore;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DebuggerDisplayAttributeShouldContainValidExpressionsAnalyzer : DiagnosticAnalyzer
{
private static readonly char[] MemberSeparators = [',', '(', '.', '[', ' '];
private static readonly Dictionary<string, SpecialType> CSharpKeywordToTypeName = new(StringComparer.Ordinal)
{
["bool"] = SpecialType.System_Boolean,
["byte"] = SpecialType.System_Byte,
["sbyte"] = SpecialType.System_SByte,
["char"] = SpecialType.System_Char,
["decimal"] = SpecialType.System_Decimal,
["double"] = SpecialType.System_Double,
["float"] = SpecialType.System_Single,
["int"] = SpecialType.System_Int32,
["uint"] = SpecialType.System_UInt32,
["nint"] = SpecialType.System_IntPtr,
["nuint"] = SpecialType.System_UIntPtr,
["long"] = SpecialType.System_Int64,
["ulong"] = SpecialType.System_UInt64,
["short"] = SpecialType.System_Int16,
["ushort"] = SpecialType.System_UInt16,
["object"] = SpecialType.System_Object,
["string"] = SpecialType.System_String,
};

private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.DebuggerDisplayAttributeShouldContainValidExpressions,
Expand Down Expand Up @@ -63,94 +88,223 @@ private static void AnalyzeNamedType(SymbolAnalysisContext context, INamedTypeSy
}
}
}
}

static bool MemberExists(INamedTypeSymbol? symbol, string name)
private static void ValidateValue(SymbolAnalysisContext context, INamedTypeSymbol symbol, AttributeData attribute, string value)
{
var expressions = ExtractExpressions(value.AsSpan());
if (expressions is null)
return;

foreach (var expression in expressions)
{
while (symbol is not null)
{
if (!symbol.GetMembers(name).IsEmpty)
return true;
if (string.IsNullOrWhiteSpace(expression))
continue;

symbol = symbol.BaseType;
var expressionSyntax = SyntaxFactory.ParseExpression(expression);
foreach (var memberPath in ExtractMemberPaths(expressionSyntax))
{
if (!IsValid(context.Compilation, symbol, memberPath, out var invalidMember))
{
context.ReportDiagnostic(Rule, attribute, invalidMember);
break;
}
}

return false;
}
}

private static List<List<string>> ExtractMemberPaths(ExpressionSyntax expressionSyntax)
{
var paths = new List<List<string>>();
ExtractMemberPaths(paths, expressionSyntax);
return paths;

static List<string>? ParseMembers(ReadOnlySpan<char> value)
static void ExtractMemberPaths(List<List<string>> results, ExpressionSyntax expressionSyntax)
{
List<string>? result = null;
var path = new List<string>();

static int IndexOf(ReadOnlySpan<char> value, char c)
while (expressionSyntax is not null)
{
var skipped = 0;
while (!value.IsEmpty)
switch (expressionSyntax)
{
var index = value.IndexOfAny(c, '\\');
if (index < 0)
return -1;

if (value[index] == c)
return index + skipped;

if (index + 1 < value.Length)
{
skipped += index + 2;
value = value[(index + 2)..];
}
else
{
return -1;
}
}
case ParenthesizedExpressionSyntax parenthesizedExpression:
// Check if parentheses are necessary
// (Instance).Member => Instance.Member
// ((Instance + value)).Member => stop evaluating
if (path.Count is 0)
{
expressionSyntax = parenthesizedExpression.Expression;
}
else
{
if (parenthesizedExpression.Expression is MemberAccessExpressionSyntax or IdentifierNameSyntax or ParenthesizedExpressionSyntax)
{
expressionSyntax = parenthesizedExpression.Expression;
}
else
{
return;
}
}

break;

case IdentifierNameSyntax identifierName:
path.Insert(0, identifierName.Identifier.ValueText);
results.Add(path);
return;

return -1;
case MemberAccessExpressionSyntax memberAccessExpression:
path.Insert(0, memberAccessExpression.Name.Identifier.ValueText);
expressionSyntax = memberAccessExpression.Expression;
break;

case InvocationExpressionSyntax invocationExpression:
foreach (var argument in invocationExpression.ArgumentList.Arguments)
{
ExtractMemberPaths(results, argument.Expression);
}

path.Clear(); // Clear the path because we don't know the return type
expressionSyntax = invocationExpression.Expression;
break;

case BinaryExpressionSyntax binaryExpression:
ExtractMemberPaths(results, binaryExpression.Left);
ExtractMemberPaths(results, binaryExpression.Right);
return;

case PrefixUnaryExpressionSyntax unaryExpression:
ExtractMemberPaths(results, unaryExpression.Operand);
return;

case PostfixUnaryExpressionSyntax unaryExpression:
ExtractMemberPaths(results, unaryExpression.Operand);
return;

case ElementAccessExpressionSyntax elementAccess:
foreach (var argument in elementAccess.ArgumentList.Arguments)
{
ExtractMemberPaths(results, argument.Expression);
}

path.Clear(); // Clear the path because we don't know the return type
expressionSyntax = elementAccess.Expression;
break;

default:
return;
}
}

while (!value.IsEmpty)
{
var startIndex = IndexOf(value, '{');
if (startIndex < 0)
break;
results.Add(path);
}
}

value = value[(startIndex + 1)..];
var endIndex = IndexOf(value, '}');
if (endIndex < 0)
break;
private static bool IsValid(Compilation compilation, ISymbol rootSymbol, List<string> syntax, out string? invalidMember)
{
if (syntax.Count is 0)
{
invalidMember = null;
return true;
}

var member = value[..endIndex];
result ??= [];
result.Add(GetMemberName(member));
var firstMember = syntax.First();
var current = FindSymbol(rootSymbol, firstMember) ?? FindGlobalSymbol(compilation, firstMember);
if (current is null)
{
invalidMember = firstMember;
return false;
}

value = value[(endIndex + 1)..];
foreach (var member in syntax.Skip(1))
{
var next = FindSymbol(current, member);
if (next is null)
{
invalidMember = member;
return false;
}

return result;
current = next;
}

invalidMember = null;
return true;

static string GetMemberName(ReadOnlySpan<char> member)
static ISymbol? FindSymbol(ISymbol parent, string name)
{
if (parent is INamespaceOrTypeSymbol typeSymbol)
{
var index = member.IndexOfAny(MemberSeparators);
if (index < 0)
return member.ToString();
if (typeSymbol.GetAllMembers(name).FirstOrDefault() is { } member)
{
if (member is INamespaceOrTypeSymbol)
return member;

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

return null;
}

static void ValidateValue(SymbolAnalysisContext context, INamedTypeSymbol symbol, AttributeData attribute, string value)
static ISymbol? FindGlobalSymbol(Compilation compilation, string name)
{
var members = ParseMembers(value.AsSpan());
if (members is not null)
if (CSharpKeywordToTypeName.TryGetValue(name, out var specialType))
return compilation.GetSpecialType(specialType);

return compilation.GlobalNamespace.GetMembers(name).FirstOrDefault();
}
}

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

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

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

var expression = value[..endIndex];
result ??= [];
result.Add(expression.ToString());

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

return result;

static int IndexOf(ReadOnlySpan<char> value, char c)
{
var skipped = 0;
while (!value.IsEmpty)
{
foreach (var member in members)
var index = value.IndexOfAny(c, '\\');
if (index < 0)
return -1;

if (value[index] == c)
return index + skipped;

if (index + 1 < value.Length)
{
if (!MemberExists(symbol, member))
{
context.ReportDiagnostic(Rule, attribute, member);
return;
}
skipped += index + 2;
value = value[(index + 2)..];
}
else
{
return -1;
}
}

return -1;
}
}
}
Loading