Skip to content

Commit

Permalink
Improved nameof() handling (#167)
Browse files Browse the repository at this point in the history
* improved `nameof()` handling
* Improve analyzer to match
  • Loading branch information
viceroypenguin authored Feb 8, 2025
1 parent d54c645 commit 0405b21
Show file tree
Hide file tree
Showing 11 changed files with 872 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,11 @@ IV0017 | ImmediateValidations | Error | ValidateClassAnalyzer
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
IV0018 | ImmediateValidations | Warning | ValidateClassAnalyzer

## Release 2.3

### Removed Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
IV0016 | ImmediateValidations | Warning | ValidateClassAnalyzer
151 changes: 108 additions & 43 deletions src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -54,22 +55,11 @@ public sealed class ValidateClassAnalyzer : DiagnosticAnalyzer
description: "Incompatible types will lead to incorrect validation code."
);

public static readonly DiagnosticDescriptor ValidateParameterPropertyIncompatibleType =
new(
id: DiagnosticIds.IV0016ValidateParameterPropertyIncompatibleType,
title: "Parameter is incompatible type",
messageFormat: "Property/parameter `{0}` is marked `[TargetType]`, but property `{1}` is not of type `{2}`",
category: "ImmediateValidations",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Incompatible types will lead to incorrect validation code."
);

public static readonly DiagnosticDescriptor ValidateParameterNameofInvalid =
new(
id: DiagnosticIds.IV0017ValidateParameterNameofInvalid,
title: "nameof() target is invalid",
messageFormat: "nameof({0}) must refer to a property or method on the class `{1}`",
messageFormat: "nameof({0}) must refer to {1}",
category: "ImmediateValidations",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
Expand All @@ -94,7 +84,6 @@ public sealed class ValidateClassAnalyzer : DiagnosticAnalyzer
IValidationTargetMissing,
ValidatePropertyIncompatibleType,
ValidateParameterIncompatibleType,
ValidateParameterPropertyIncompatibleType,
ValidateParameterNameofInvalid,
ValidateNotNullWhenInvalid,
]);
Expand Down Expand Up @@ -427,62 +416,138 @@ List<ISymbol> members
var validateParameter = validateParameterSymbols
.First(p => string.Equals(p.Name, parameter.Name, StringComparison.Ordinal));

if (syntax.Expression.IsNameOfExpression(out var propertyName))
var argumentType = GetArgumentType(
context,
syntax,
members
);

if (argumentType is null)
return;

if (ValidateArgumentType(context.Compilation, validateParameter, argumentType, typeArgumentType, out var targetType))
return;

context.ReportDiagnostic(
Diagnostic.Create(
ValidateParameterIncompatibleType,
syntax.GetLocation(),
parameter.Name,
targetType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)
)
);
}

private static ITypeSymbol? GetArgumentType(
SyntaxNodeAnalysisContext context,
AttributeArgumentSyntax syntax,
List<ISymbol> members
)
{
if (syntax.Expression.IsNameOfExpression(out var argumentExpression))
{
var argumentSymbol = GetArgumentSymbol(
context,
syntax,
argumentExpression,
members
);

if (argumentSymbol is null)
return null;

var argumentType = argumentSymbol switch
{
IMethodSymbol { ReturnType: { } t } => t,
IPropertySymbol { Type: { } t } => t,
IFieldSymbol { Type: { } t } => t,
_ => throw new InvalidOperationException(),
};

return argumentType;
}
else
{
if (context.SemanticModel.GetOperation(syntax.Expression)?.Type is not ITypeSymbol argumentType)
return null;

return argumentType;
}
}

private static ISymbol? GetArgumentSymbol(
SyntaxNodeAnalysisContext context,
AttributeArgumentSyntax syntax,
ExpressionSyntax argumentExpression,
List<ISymbol> members
)
{
if (argumentExpression is SimpleNameSyntax { Identifier.ValueText: { } name })
{
var member = members
.Find(p => string.Equals(p.Name, propertyName, StringComparison.Ordinal));
.Find(p => string.Equals(p.Name, name, StringComparison.Ordinal));

if (member is null)
{
context.ReportDiagnostic(
Diagnostic.Create(
ValidateParameterNameofInvalid,
syntax.GetLocation(),
propertyName,
context.ContainingSymbol!.Name
name,
FormattableString.Invariant($"a property or method on the class `{context.ContainingSymbol!.Name}`")
)
);

return;
return null;
}

var memberType = member switch
{
IMethodSymbol { ReturnType: { } t } => t,
IPropertySymbol { Type: { } t } => t,
IFieldSymbol { Type: { } t } => t,
_ => throw new InvalidOperationException(),
};
return member;
}
else
{
var symbolInfo = context.SemanticModel.GetSymbolInfo(argumentExpression);

if (!ValidateArgumentType(context.Compilation, validateParameter, memberType, typeArgumentType, out var targetType))
var symbol = symbolInfo.Symbol
?? symbolInfo.CandidateSymbols
.FirstOrDefault(
ims => ims is IMethodSymbol
{
Parameters: []
}
);

if (symbol is null)
return null;

if (symbol is not { Kind: SymbolKind.Field or SymbolKind.Method or SymbolKind.Property })
{
context.ReportDiagnostic(
Diagnostic.Create(
ValidateParameterPropertyIncompatibleType,
ValidateParameterNameofInvalid,
syntax.GetLocation(),
parameter.Name,
member.Name,
targetType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)
symbol.Name,
FormattableString.Invariant($"a property or method on the class `{context.ContainingSymbol!.Name}` or a static member of another class")
)
);

return null;
}
}
else
{
if (context.SemanticModel.GetOperation(syntax.Expression)?.Type is not ITypeSymbol argumentType)
return;

if (!ValidateArgumentType(context.Compilation, validateParameter, argumentType, typeArgumentType, out var targetType))
if (symbol is not { IsStatic: true })
{
context.ReportDiagnostic(
Diagnostic.Create(
ValidateParameterIncompatibleType,
ValidateParameterNameofInvalid,
syntax.GetLocation(),
parameter.Name,
targetType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)
symbol.Name,
FormattableString.Invariant($"a static member of the class `{symbol.ContainingType.Name}`")
)
);

return null;
}

return symbol;
}
}

Expand Down Expand Up @@ -527,21 +592,21 @@ file static class Extensions
public static bool IsTargetTypeSymbol(this ISymbol symbol) =>
symbol.GetAttributes().Any(a => a.AttributeClass.IsTargetTypeAttribute());

public static bool IsNameOfExpression(this ExpressionSyntax syntax, out string? name)
public static bool IsNameOfExpression(this ExpressionSyntax syntax, [NotNullWhen(returnValue: true)] out ExpressionSyntax? argumentExpression)
{
if (syntax is InvocationExpressionSyntax
{
Expression: SimpleNameSyntax { Identifier.ValueText: "nameof" },
ArgumentList.Arguments: [{ Expression: SimpleNameSyntax { Identifier.ValueText: var n } }],
ArgumentList.Arguments: [{ Expression: { } expr }],
}
)
{
name = n;
argumentExpression = expr;
return true;
}
else
{
name = null;
argumentExpression = null;
return false;
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/Immediate.Validations.Generators/DisplayNameFormatters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.CodeAnalysis;

namespace Immediate.Validations.Generators;

internal static class DisplayNameFormatters
{
public static readonly SymbolDisplayFormat FullyQualifiedForMembers =
SymbolDisplayFormat.FullyQualifiedFormat
.WithMemberOptions(
SymbolDisplayMemberOptions.IncludeContainingType
| SymbolDisplayMemberOptions.IncludeParameters
);
}
2 changes: 1 addition & 1 deletion src/Immediate.Validations.Generators/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static Template GetTemplate(string name)
new(
@"(?<=[^A-Z])([A-Z])",
RegexOptions.Compiled,
matchTimeout: TimeSpan.FromMilliseconds(10)
matchTimeout: TimeSpan.FromSeconds(1)
);

public static string ToTitleCase(this string pascalCase) =>
Expand Down
Loading

0 comments on commit 0405b21

Please sign in to comment.