From 32fa5a313de974ad0fcdb994d3350da0e4c55672 Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:16:33 +0200 Subject: [PATCH 01/17] Extract rule argument shape --- .../Analysis/Rules/RuleArgumentAnalyzer.cs | 27 +-------- .../Analysis/Rules/RuleInvocationAnalyzer.cs | 59 +------------------ .../Analysis/Rules/RuleShape.cs | 38 ++++++++++++ 3 files changed, 43 insertions(+), 81 deletions(-) create mode 100644 src/TinyValidations.SourceGen/Analysis/Rules/RuleShape.cs diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleArgumentAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RuleArgumentAnalyzer.cs index 3b75fac..f78bd8a 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleArgumentAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RuleArgumentAnalyzer.cs @@ -7,17 +7,17 @@ internal sealed class RuleArgumentAnalyzer { public string GetRuleArgument(RuleKind kind, InvocationExpressionSyntax invocation) { - if (!RequiresValueArgument(kind)) + if (!RuleShape.RequiresValueArgument(kind)) { return string.Empty; } - return GetArgument(invocation, 1); + return GetArgument(invocation, RuleShape.ValueArgumentIndex(kind)); } public string GetMessage(RuleKind kind, InvocationExpressionSyntax invocation) { - var messageIndex = GetMessageArgumentIndex(kind); + var messageIndex = RuleShape.MessageArgumentIndex(kind); return GetArgument(invocation, messageIndex); } @@ -31,30 +31,9 @@ private static string GetArgument(InvocationExpressionSyntax invocation, int arg return invocation.ArgumentList.Arguments[argumentIndex].Expression.ToString(); } - private static int GetMessageArgumentIndex(RuleKind kind) - { - if (RequiresValueArgument(kind)) - { - return 2; - } - - return 1; - } - private static bool HasArgument(InvocationExpressionSyntax invocation, int argumentIndex) { return invocation.ArgumentList.Arguments.Count > argumentIndex; } - - private static bool RequiresValueArgument(RuleKind kind) - { - return kind == RuleKind.TextLengthAtLeast - || kind == RuleKind.TextLengthAtMost - || kind == RuleKind.Above - || kind == RuleKind.AtLeast - || kind == RuleKind.Below - || kind == RuleKind.AtMost - || kind == RuleKind.Matches; - } } } diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs index 1f64f56..752afa9 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs @@ -202,7 +202,7 @@ private static bool IsValidationRulesInvocation( private static bool HasUnsupportedArgument(RuleKind kind, InvocationExpressionSyntax invocation) { - var valueArgumentIndex = GetValueArgumentIndex(kind); + var valueArgumentIndex = RuleShape.ValueArgumentIndex(kind); if (valueArgumentIndex >= 0) { if (!IsSupportedArgument(invocation, valueArgumentIndex)) @@ -211,7 +211,7 @@ private static bool HasUnsupportedArgument(RuleKind kind, InvocationExpressionSy } } - var messageArgumentIndex = GetMessageArgumentIndex(kind); + var messageArgumentIndex = RuleShape.MessageArgumentIndex(kind); if (HasArgument(invocation, messageArgumentIndex)) { if (!IsSupportedArgument(invocation, messageArgumentIndex)) @@ -223,26 +223,6 @@ private static bool HasUnsupportedArgument(RuleKind kind, InvocationExpressionSy return false; } - private static int GetValueArgumentIndex(RuleKind kind) - { - if (RequiresValueArgument(kind)) - { - return 1; - } - - return -1; - } - - private static int GetMessageArgumentIndex(RuleKind kind) - { - if (RequiresValueArgument(kind)) - { - return 2; - } - - return 1; - } - private static bool HasArgument(InvocationExpressionSyntax invocation, int argumentIndex) { return invocation.ArgumentList.Arguments.Count > argumentIndex; @@ -312,41 +292,6 @@ private static string GetRequirementMethodName(IMethodSymbol method) return containingType + "." + method.Name; } - private static bool RequiresValueArgument(RuleKind kind) - { - if (kind == RuleKind.TextLengthAtLeast) - { - return true; - } - - if (kind == RuleKind.TextLengthAtMost) - { - return true; - } - - if (kind == RuleKind.Above) - { - return true; - } - - if (kind == RuleKind.AtLeast) - { - return true; - } - - if (kind == RuleKind.Below) - { - return true; - } - - if (kind == RuleKind.AtMost) - { - return true; - } - - return kind == RuleKind.Matches; - } - private static bool IsValidCustomRule(ITypeSymbol? typeSymbol, INamedTypeSymbol commandType) { if (!(typeSymbol is INamedTypeSymbol namedType)) diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleShape.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RuleShape.cs new file mode 100644 index 0000000..f51fa83 --- /dev/null +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RuleShape.cs @@ -0,0 +1,38 @@ +using TinyValidations.SourceGen.Model; + +namespace TinyValidations.SourceGen.Analysis.Rules +{ + internal static class RuleShape + { + public static bool RequiresValueArgument(RuleKind kind) + { + return kind == RuleKind.TextLengthAtLeast + || kind == RuleKind.TextLengthAtMost + || kind == RuleKind.Above + || kind == RuleKind.AtLeast + || kind == RuleKind.Below + || kind == RuleKind.AtMost + || kind == RuleKind.Matches; + } + + public static int ValueArgumentIndex(RuleKind kind) + { + if (RequiresValueArgument(kind)) + { + return 1; + } + + return -1; + } + + public static int MessageArgumentIndex(RuleKind kind) + { + if (RequiresValueArgument(kind)) + { + return 2; + } + + return 1; + } + } +} From f21c978738b337ffbc780d9ef62844623e2f63ab Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:19:09 +0200 Subject: [PATCH 02/17] Extract custom rule analyzer --- .../Analysis/Rules/CustomRuleAnalyzer.cs | 104 ++++++++++++++++++ .../Analysis/Rules/RuleInvocationAnalyzer.cs | 92 +--------------- 2 files changed, 106 insertions(+), 90 deletions(-) create mode 100644 src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs new file mode 100644 index 0000000..0f5dd71 --- /dev/null +++ b/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs @@ -0,0 +1,104 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TinyValidations.SourceGen.Model; +using TinyValidations.SourceGen.Validation; + +namespace TinyValidations.SourceGen.Analysis.Rules +{ + internal sealed class CustomRuleAnalyzer + { + public RuleAnalysisResult Analyze( + SemanticModel semanticModel, + SimpleNameSyntax methodName, + INamedTypeSymbol commandType) + { + if (!(methodName is GenericNameSyntax genericName)) + { + return InvalidCustomRule(methodName, methodName.ToString()); + } + + if (!HasSingleTypeArgument(genericName)) + { + return InvalidCustomRule(genericName, genericName.ToString()); + } + + var typeSyntax = genericName.TypeArgumentList.Arguments[0]; + var typeSymbol = semanticModel.GetTypeInfo(typeSyntax).Type; + if (!IsValidCustomRule(typeSymbol, commandType)) + { + return InvalidCustomRule(typeSyntax, typeSyntax.ToString()); + } + + var customRuleType = GetTypeName(typeSyntax, typeSymbol); + + return RuleAnalysisResult.ForRule(new RuleDefinition( + RuleKind.Use, + string.Empty, + string.Empty, + string.Empty, + string.Empty, + customRuleType)); + } + + private static RuleAnalysisResult InvalidCustomRule(SyntaxNode syntax, string value) + { + return RuleAnalysisResult.ForIssue(new ValidationIssue( + ValidationDiagnostics.InvalidCustomRule, + syntax.GetLocation(), + value)); + } + + private static bool HasSingleTypeArgument(GenericNameSyntax genericName) + { + return genericName.TypeArgumentList.Arguments.Count == 1; + } + + private static string GetTypeName(TypeSyntax typeSyntax, ITypeSymbol? typeSymbol) + { + if (typeSymbol == null) + { + return typeSyntax.ToString(); + } + + return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + private static bool IsValidCustomRule(ITypeSymbol? typeSymbol, INamedTypeSymbol commandType) + { + if (!(typeSymbol is INamedTypeSymbol namedType)) + { + return false; + } + + foreach (var candidate in namedType.AllInterfaces) + { + if (IsAsyncValidationRule(candidate, commandType)) + { + return true; + } + } + + return false; + } + + private static bool IsAsyncValidationRule(INamedTypeSymbol candidate, INamedTypeSymbol commandType) + { + if (candidate.ContainingNamespace.ToDisplayString() != "TinyValidations") + { + return false; + } + + if (candidate.Name != "IAsyncValidationRule") + { + return false; + } + + if (candidate.TypeArguments.Length != 1) + { + return false; + } + + return SymbolEqualityComparer.Default.Equals(candidate.TypeArguments[0], commandType); + } + } +} diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs index 752afa9..37249bf 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs @@ -10,6 +10,7 @@ internal sealed class RuleInvocationAnalyzer private readonly RuleMethodMap _methodMap = new RuleMethodMap(); private readonly MemberAccessAnalyzer _memberAccessAnalyzer = new MemberAccessAnalyzer(); private readonly RuleArgumentAnalyzer _argumentAnalyzer = new RuleArgumentAnalyzer(); + private readonly CustomRuleAnalyzer _customRuleAnalyzer = new CustomRuleAnalyzer(); public RuleAnalysisResult? Analyze( SemanticModel semanticModel, @@ -39,7 +40,7 @@ internal sealed class RuleInvocationAnalyzer if (kind.Value == RuleKind.Use) { - return AnalyzeCustomRule(semanticModel, invocation, memberAccess.Name, commandType); + return _customRuleAnalyzer.Analyze(semanticModel, memberAccess.Name, commandType); } if (kind.Value == RuleKind.Requires) @@ -127,58 +128,6 @@ private RuleAnalysisResult AnalyzeRequiresRule( requirementMethod)); } - private static RuleAnalysisResult AnalyzeCustomRule( - SemanticModel semanticModel, - InvocationExpressionSyntax invocation, - SimpleNameSyntax methodName, - INamedTypeSymbol commandType) - { - if (!(methodName is GenericNameSyntax genericName)) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.InvalidCustomRule, - methodName.GetLocation(), - methodName.ToString())); - } - - if (!HasSingleTypeArgument(genericName)) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.InvalidCustomRule, - genericName.GetLocation(), - genericName.ToString())); - } - - var typeSyntax = genericName.TypeArgumentList.Arguments[0]; - var typeSymbol = semanticModel.GetTypeInfo(typeSyntax).Type; - if (!IsValidCustomRule(typeSymbol, commandType)) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.InvalidCustomRule, - typeSyntax.GetLocation(), - typeSyntax.ToString())); - } - - var customRuleType = GetTypeName(typeSyntax, typeSymbol); - - return RuleAnalysisResult.ForRule(new RuleDefinition(RuleKind.Use, string.Empty, string.Empty, string.Empty, string.Empty, customRuleType)); - } - - private static bool HasSingleTypeArgument(GenericNameSyntax genericName) - { - return genericName.TypeArgumentList.Arguments.Count == 1; - } - - private static string GetTypeName(TypeSyntax typeSyntax, ITypeSymbol? typeSymbol) - { - if (typeSymbol == null) - { - return typeSyntax.ToString(); - } - - return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - } - private static bool IsValidationRulesInvocation( SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccess, @@ -292,42 +241,5 @@ private static string GetRequirementMethodName(IMethodSymbol method) return containingType + "." + method.Name; } - private static bool IsValidCustomRule(ITypeSymbol? typeSymbol, INamedTypeSymbol commandType) - { - if (!(typeSymbol is INamedTypeSymbol namedType)) - { - return false; - } - - foreach (var candidate in namedType.AllInterfaces) - { - if (IsAsyncValidationRule(candidate, commandType)) - { - return true; - } - } - - return false; - } - - private static bool IsAsyncValidationRule(INamedTypeSymbol candidate, INamedTypeSymbol commandType) - { - if (candidate.ContainingNamespace.ToDisplayString() != "TinyValidations") - { - return false; - } - - if (candidate.Name != "IAsyncValidationRule") - { - return false; - } - - if (candidate.TypeArguments.Length != 1) - { - return false; - } - - return SymbolEqualityComparer.Default.Equals(candidate.TypeArguments[0], commandType); - } } } From 3d5c09edeedbc24b3fbde687d1ca510457b469a8 Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:20:51 +0200 Subject: [PATCH 03/17] Extract requires rule analyzer --- .../Analysis/Rules/RequiresRuleAnalyzer.cs | 140 ++++++++++++++++++ .../Analysis/Rules/RuleInvocationAnalyzer.cs | 106 +------------ 2 files changed, 142 insertions(+), 104 deletions(-) create mode 100644 src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs new file mode 100644 index 0000000..2ac3d68 --- /dev/null +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs @@ -0,0 +1,140 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TinyValidations.SourceGen.Model; +using TinyValidations.SourceGen.Validation; + +namespace TinyValidations.SourceGen.Analysis.Rules +{ + internal sealed class RequiresRuleAnalyzer + { + private readonly MemberAccessAnalyzer _memberAccessAnalyzer = new MemberAccessAnalyzer(); + + public RuleAnalysisResult Analyze( + SemanticModel semanticModel, + InvocationExpressionSyntax invocation) + { + if (invocation.ArgumentList.Arguments.Count < 3) + { + return UnsupportedArgument(invocation, invocation.ToString()); + } + + var member = _memberAccessAnalyzer.Analyze(invocation.ArgumentList.Arguments[0].Expression); + if (member == null) + { + return UnsupportedSelector( + invocation.ArgumentList.Arguments[0], + invocation.ArgumentList.Arguments[0].Expression.ToString()); + } + + if (!IsSupportedRequirementMethod(semanticModel, invocation.ArgumentList.Arguments[1].Expression, out var requirementMethod)) + { + return UnsupportedArgument( + invocation.ArgumentList.Arguments[1], + invocation.ArgumentList.Arguments[1].Expression.ToString()); + } + + if (!IsSupportedArgument(invocation, 2)) + { + return UnsupportedArgument( + invocation.ArgumentList.Arguments[2], + invocation.ArgumentList.Arguments[2].Expression.ToString()); + } + + var message = invocation.ArgumentList.Arguments[2].Expression.ToString(); + + return RuleAnalysisResult.ForRule(new RuleDefinition( + RuleKind.Requires, + member.Path, + member.Access, + string.Empty, + message, + string.Empty, + requirementMethod)); + } + + private static RuleAnalysisResult UnsupportedSelector(SyntaxNode syntax, string value) + { + return RuleAnalysisResult.ForIssue(new ValidationIssue( + ValidationDiagnostics.UnsupportedSelector, + syntax.GetLocation(), + value)); + } + + private static RuleAnalysisResult UnsupportedArgument(SyntaxNode syntax, string value) + { + return RuleAnalysisResult.ForIssue(new ValidationIssue( + ValidationDiagnostics.UnsupportedArgument, + syntax.GetLocation(), + value)); + } + + private static bool IsSupportedArgument(InvocationExpressionSyntax invocation, int argumentIndex) + { + if (!HasArgument(invocation, argumentIndex)) + { + return false; + } + + return invocation.ArgumentList.Arguments[argumentIndex].Expression is LiteralExpressionSyntax; + } + + private static bool HasArgument(InvocationExpressionSyntax invocation, int argumentIndex) + { + return invocation.ArgumentList.Arguments.Count > argumentIndex; + } + + private static bool IsSupportedRequirementMethod( + SemanticModel semanticModel, + ExpressionSyntax expression, + out string requirementMethod) + { + requirementMethod = string.Empty; + + var symbolInfo = semanticModel.GetSymbolInfo(expression); + var symbol = symbolInfo.Symbol ?? GetSingleCandidate(symbolInfo); + if (!(symbol is IMethodSymbol method)) + { + return false; + } + + if (!method.IsStatic) + { + return false; + } + + if (method.TypeArguments.Length != 0) + { + return false; + } + + if (method.Parameters.Length != 1) + { + return false; + } + + if (method.ReturnType.SpecialType != SpecialType.System_Boolean) + { + return false; + } + + requirementMethod = GetRequirementMethodName(method); + return true; + } + + private static ISymbol? GetSingleCandidate(SymbolInfo symbolInfo) + { + if (symbolInfo.CandidateSymbols.Length != 1) + { + return null; + } + + return symbolInfo.CandidateSymbols[0]; + } + + private static string GetRequirementMethodName(IMethodSymbol method) + { + var containingType = method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + return containingType + "." + method.Name; + } + } +} diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs index 37249bf..3719e4f 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs @@ -11,6 +11,7 @@ internal sealed class RuleInvocationAnalyzer private readonly MemberAccessAnalyzer _memberAccessAnalyzer = new MemberAccessAnalyzer(); private readonly RuleArgumentAnalyzer _argumentAnalyzer = new RuleArgumentAnalyzer(); private readonly CustomRuleAnalyzer _customRuleAnalyzer = new CustomRuleAnalyzer(); + private readonly RequiresRuleAnalyzer _requiresRuleAnalyzer = new RequiresRuleAnalyzer(); public RuleAnalysisResult? Analyze( SemanticModel semanticModel, @@ -45,7 +46,7 @@ internal sealed class RuleInvocationAnalyzer if (kind.Value == RuleKind.Requires) { - return AnalyzeRequiresRule(semanticModel, invocation); + return _requiresRuleAnalyzer.Analyze(semanticModel, invocation); } if (invocation.ArgumentList.Arguments.Count == 0) @@ -79,55 +80,6 @@ internal sealed class RuleInvocationAnalyzer return RuleAnalysisResult.ForRule(new RuleDefinition(kind.Value, member.Path, member.Access, argument, message, string.Empty)); } - private RuleAnalysisResult AnalyzeRequiresRule( - SemanticModel semanticModel, - InvocationExpressionSyntax invocation) - { - if (invocation.ArgumentList.Arguments.Count < 3) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedArgument, - invocation.GetLocation(), - invocation.ToString())); - } - - var member = _memberAccessAnalyzer.Analyze(invocation.ArgumentList.Arguments[0].Expression); - if (member == null) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedSelector, - invocation.ArgumentList.Arguments[0].GetLocation(), - invocation.ArgumentList.Arguments[0].Expression.ToString())); - } - - if (!IsSupportedRequirementMethod(semanticModel, invocation.ArgumentList.Arguments[1].Expression, out var requirementMethod)) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedArgument, - invocation.ArgumentList.Arguments[1].GetLocation(), - invocation.ArgumentList.Arguments[1].Expression.ToString())); - } - - if (!IsSupportedArgument(invocation, 2)) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedArgument, - invocation.ArgumentList.Arguments[2].GetLocation(), - invocation.ArgumentList.Arguments[2].Expression.ToString())); - } - - var message = invocation.ArgumentList.Arguments[2].Expression.ToString(); - - return RuleAnalysisResult.ForRule(new RuleDefinition( - RuleKind.Requires, - member.Path, - member.Access, - string.Empty, - message, - string.Empty, - requirementMethod)); - } - private static bool IsValidationRulesInvocation( SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccess, @@ -187,59 +139,5 @@ private static bool IsSupportedArgument(InvocationExpressionSyntax invocation, i return invocation.ArgumentList.Arguments[argumentIndex].Expression is LiteralExpressionSyntax; } - private static bool IsSupportedRequirementMethod( - SemanticModel semanticModel, - ExpressionSyntax expression, - out string requirementMethod) - { - requirementMethod = string.Empty; - - var symbolInfo = semanticModel.GetSymbolInfo(expression); - var symbol = symbolInfo.Symbol ?? GetSingleCandidate(symbolInfo); - if (!(symbol is IMethodSymbol method)) - { - return false; - } - - if (!method.IsStatic) - { - return false; - } - - if (method.TypeArguments.Length != 0) - { - return false; - } - - if (method.Parameters.Length != 1) - { - return false; - } - - if (method.ReturnType.SpecialType != SpecialType.System_Boolean) - { - return false; - } - - requirementMethod = GetRequirementMethodName(method); - return true; - } - - private static ISymbol? GetSingleCandidate(SymbolInfo symbolInfo) - { - if (symbolInfo.CandidateSymbols.Length != 1) - { - return null; - } - - return symbolInfo.CandidateSymbols[0]; - } - - private static string GetRequirementMethodName(IMethodSymbol method) - { - var containingType = method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - return containingType + "." + method.Name; - } - } } From 80c3a73b5f837d79b0fd648e0f217e56e24a084c Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:22:33 +0200 Subject: [PATCH 04/17] Extract member rule analyzer --- .../Analysis/Rules/MemberRuleAnalyzer.cs | 89 +++++++++++++++++++ .../Analysis/Rules/RuleInvocationAnalyzer.cs | 71 +-------------- 2 files changed, 91 insertions(+), 69 deletions(-) create mode 100644 src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs new file mode 100644 index 0000000..d1c02f1 --- /dev/null +++ b/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs @@ -0,0 +1,89 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using TinyValidations.SourceGen.Model; +using TinyValidations.SourceGen.Validation; + +namespace TinyValidations.SourceGen.Analysis.Rules +{ + internal sealed class MemberRuleAnalyzer + { + private readonly MemberAccessAnalyzer _memberAccessAnalyzer = new MemberAccessAnalyzer(); + private readonly RuleArgumentAnalyzer _argumentAnalyzer = new RuleArgumentAnalyzer(); + + public RuleAnalysisResult Analyze(RuleKind kind, InvocationExpressionSyntax invocation) + { + if (invocation.ArgumentList.Arguments.Count == 0) + { + return RuleAnalysisResult.ForIssue(new ValidationIssue( + ValidationDiagnostics.UnsupportedSelector, + invocation.GetLocation(), + invocation.ToString())); + } + + var member = _memberAccessAnalyzer.Analyze(invocation.ArgumentList.Arguments[0].Expression); + if (member == null) + { + return RuleAnalysisResult.ForIssue(new ValidationIssue( + ValidationDiagnostics.UnsupportedSelector, + invocation.ArgumentList.Arguments[0].GetLocation(), + invocation.ArgumentList.Arguments[0].Expression.ToString())); + } + + if (HasUnsupportedArgument(kind, invocation)) + { + return RuleAnalysisResult.ForIssue(new ValidationIssue( + ValidationDiagnostics.UnsupportedArgument, + invocation.GetLocation(), + invocation.ToString())); + } + + var argument = _argumentAnalyzer.GetRuleArgument(kind, invocation); + var message = _argumentAnalyzer.GetMessage(kind, invocation); + + return RuleAnalysisResult.ForRule(new RuleDefinition( + kind, + member.Path, + member.Access, + argument, + message, + string.Empty)); + } + + private static bool HasUnsupportedArgument(RuleKind kind, InvocationExpressionSyntax invocation) + { + var valueArgumentIndex = RuleShape.ValueArgumentIndex(kind); + if (valueArgumentIndex >= 0) + { + if (!IsSupportedArgument(invocation, valueArgumentIndex)) + { + return true; + } + } + + var messageArgumentIndex = RuleShape.MessageArgumentIndex(kind); + if (HasArgument(invocation, messageArgumentIndex)) + { + if (!IsSupportedArgument(invocation, messageArgumentIndex)) + { + return true; + } + } + + return false; + } + + private static bool IsSupportedArgument(InvocationExpressionSyntax invocation, int argumentIndex) + { + if (!HasArgument(invocation, argumentIndex)) + { + return false; + } + + return invocation.ArgumentList.Arguments[argumentIndex].Expression is LiteralExpressionSyntax; + } + + private static bool HasArgument(InvocationExpressionSyntax invocation, int argumentIndex) + { + return invocation.ArgumentList.Arguments.Count > argumentIndex; + } + } +} diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs index 3719e4f..d391bbe 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs @@ -8,8 +8,7 @@ namespace TinyValidations.SourceGen.Analysis.Rules internal sealed class RuleInvocationAnalyzer { private readonly RuleMethodMap _methodMap = new RuleMethodMap(); - private readonly MemberAccessAnalyzer _memberAccessAnalyzer = new MemberAccessAnalyzer(); - private readonly RuleArgumentAnalyzer _argumentAnalyzer = new RuleArgumentAnalyzer(); + private readonly MemberRuleAnalyzer _memberRuleAnalyzer = new MemberRuleAnalyzer(); private readonly CustomRuleAnalyzer _customRuleAnalyzer = new CustomRuleAnalyzer(); private readonly RequiresRuleAnalyzer _requiresRuleAnalyzer = new RequiresRuleAnalyzer(); @@ -49,35 +48,7 @@ internal sealed class RuleInvocationAnalyzer return _requiresRuleAnalyzer.Analyze(semanticModel, invocation); } - if (invocation.ArgumentList.Arguments.Count == 0) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedSelector, - invocation.GetLocation(), - invocation.ToString())); - } - - var member = _memberAccessAnalyzer.Analyze(invocation.ArgumentList.Arguments[0].Expression); - if (member == null) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedSelector, - invocation.ArgumentList.Arguments[0].GetLocation(), - invocation.ArgumentList.Arguments[0].Expression.ToString())); - } - - if (HasUnsupportedArgument(kind.Value, invocation)) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedArgument, - invocation.GetLocation(), - invocation.ToString())); - } - - var argument = _argumentAnalyzer.GetRuleArgument(kind.Value, invocation); - var message = _argumentAnalyzer.GetMessage(kind.Value, invocation); - - return RuleAnalysisResult.ForRule(new RuleDefinition(kind.Value, member.Path, member.Access, argument, message, string.Empty)); + return _memberRuleAnalyzer.Analyze(kind.Value, invocation); } private static bool IsValidationRulesInvocation( @@ -101,43 +72,5 @@ private static bool IsValidationRulesInvocation( return SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, validationRules); } - private static bool HasUnsupportedArgument(RuleKind kind, InvocationExpressionSyntax invocation) - { - var valueArgumentIndex = RuleShape.ValueArgumentIndex(kind); - if (valueArgumentIndex >= 0) - { - if (!IsSupportedArgument(invocation, valueArgumentIndex)) - { - return true; - } - } - - var messageArgumentIndex = RuleShape.MessageArgumentIndex(kind); - if (HasArgument(invocation, messageArgumentIndex)) - { - if (!IsSupportedArgument(invocation, messageArgumentIndex)) - { - return true; - } - } - - return false; - } - - private static bool HasArgument(InvocationExpressionSyntax invocation, int argumentIndex) - { - return invocation.ArgumentList.Arguments.Count > argumentIndex; - } - - private static bool IsSupportedArgument(InvocationExpressionSyntax invocation, int argumentIndex) - { - if (!HasArgument(invocation, argumentIndex)) - { - return false; - } - - return invocation.ArgumentList.Arguments[argumentIndex].Expression is LiteralExpressionSyntax; - } - } } From 4ec2160fb04c5e85eda579d180d74542304dbfaa Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:24:38 +0200 Subject: [PATCH 05/17] Centralize rule analysis issues --- .../Analysis/Rules/CustomRuleAnalyzer.cs | 15 ++----- .../Analysis/Rules/MemberRuleAnalyzer.cs | 18 +++------ .../Analysis/Rules/RequiresRuleAnalyzer.cs | 25 ++---------- .../Analysis/Rules/RuleAnalysisIssue.cs | 40 +++++++++++++++++++ .../Analysis/Rules/RuleInvocationAnalyzer.cs | 6 +-- 5 files changed, 53 insertions(+), 51 deletions(-) create mode 100644 src/TinyValidations.SourceGen/Analysis/Rules/RuleAnalysisIssue.cs diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs index 0f5dd71..037cdf6 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs @@ -1,7 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using TinyValidations.SourceGen.Model; -using TinyValidations.SourceGen.Validation; namespace TinyValidations.SourceGen.Analysis.Rules { @@ -14,19 +13,19 @@ public RuleAnalysisResult Analyze( { if (!(methodName is GenericNameSyntax genericName)) { - return InvalidCustomRule(methodName, methodName.ToString()); + return RuleAnalysisIssue.InvalidCustomRule(methodName, methodName.ToString()); } if (!HasSingleTypeArgument(genericName)) { - return InvalidCustomRule(genericName, genericName.ToString()); + return RuleAnalysisIssue.InvalidCustomRule(genericName, genericName.ToString()); } var typeSyntax = genericName.TypeArgumentList.Arguments[0]; var typeSymbol = semanticModel.GetTypeInfo(typeSyntax).Type; if (!IsValidCustomRule(typeSymbol, commandType)) { - return InvalidCustomRule(typeSyntax, typeSyntax.ToString()); + return RuleAnalysisIssue.InvalidCustomRule(typeSyntax, typeSyntax.ToString()); } var customRuleType = GetTypeName(typeSyntax, typeSymbol); @@ -40,14 +39,6 @@ public RuleAnalysisResult Analyze( customRuleType)); } - private static RuleAnalysisResult InvalidCustomRule(SyntaxNode syntax, string value) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.InvalidCustomRule, - syntax.GetLocation(), - value)); - } - private static bool HasSingleTypeArgument(GenericNameSyntax genericName) { return genericName.TypeArgumentList.Arguments.Count == 1; diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs index d1c02f1..32c8391 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs @@ -1,6 +1,5 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using TinyValidations.SourceGen.Model; -using TinyValidations.SourceGen.Validation; namespace TinyValidations.SourceGen.Analysis.Rules { @@ -13,27 +12,20 @@ public RuleAnalysisResult Analyze(RuleKind kind, InvocationExpressionSyntax invo { if (invocation.ArgumentList.Arguments.Count == 0) { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedSelector, - invocation.GetLocation(), - invocation.ToString())); + return RuleAnalysisIssue.UnsupportedSelector(invocation, invocation.ToString()); } var member = _memberAccessAnalyzer.Analyze(invocation.ArgumentList.Arguments[0].Expression); if (member == null) { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedSelector, - invocation.ArgumentList.Arguments[0].GetLocation(), - invocation.ArgumentList.Arguments[0].Expression.ToString())); + return RuleAnalysisIssue.UnsupportedSelector( + invocation.ArgumentList.Arguments[0], + invocation.ArgumentList.Arguments[0].Expression.ToString()); } if (HasUnsupportedArgument(kind, invocation)) { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedArgument, - invocation.GetLocation(), - invocation.ToString())); + return RuleAnalysisIssue.UnsupportedArgument(invocation, invocation.ToString()); } var argument = _argumentAnalyzer.GetRuleArgument(kind, invocation); diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs index 2ac3d68..7d5100f 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs @@ -1,7 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using TinyValidations.SourceGen.Model; -using TinyValidations.SourceGen.Validation; namespace TinyValidations.SourceGen.Analysis.Rules { @@ -15,27 +14,27 @@ public RuleAnalysisResult Analyze( { if (invocation.ArgumentList.Arguments.Count < 3) { - return UnsupportedArgument(invocation, invocation.ToString()); + return RuleAnalysisIssue.UnsupportedArgument(invocation, invocation.ToString()); } var member = _memberAccessAnalyzer.Analyze(invocation.ArgumentList.Arguments[0].Expression); if (member == null) { - return UnsupportedSelector( + return RuleAnalysisIssue.UnsupportedSelector( invocation.ArgumentList.Arguments[0], invocation.ArgumentList.Arguments[0].Expression.ToString()); } if (!IsSupportedRequirementMethod(semanticModel, invocation.ArgumentList.Arguments[1].Expression, out var requirementMethod)) { - return UnsupportedArgument( + return RuleAnalysisIssue.UnsupportedArgument( invocation.ArgumentList.Arguments[1], invocation.ArgumentList.Arguments[1].Expression.ToString()); } if (!IsSupportedArgument(invocation, 2)) { - return UnsupportedArgument( + return RuleAnalysisIssue.UnsupportedArgument( invocation.ArgumentList.Arguments[2], invocation.ArgumentList.Arguments[2].Expression.ToString()); } @@ -52,22 +51,6 @@ public RuleAnalysisResult Analyze( requirementMethod)); } - private static RuleAnalysisResult UnsupportedSelector(SyntaxNode syntax, string value) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedSelector, - syntax.GetLocation(), - value)); - } - - private static RuleAnalysisResult UnsupportedArgument(SyntaxNode syntax, string value) - { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedArgument, - syntax.GetLocation(), - value)); - } - private static bool IsSupportedArgument(InvocationExpressionSyntax invocation, int argumentIndex) { if (!HasArgument(invocation, argumentIndex)) diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleAnalysisIssue.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RuleAnalysisIssue.cs new file mode 100644 index 0000000..c02c4c7 --- /dev/null +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RuleAnalysisIssue.cs @@ -0,0 +1,40 @@ +using Microsoft.CodeAnalysis; +using TinyValidations.SourceGen.Model; +using TinyValidations.SourceGen.Validation; + +namespace TinyValidations.SourceGen.Analysis.Rules +{ + internal static class RuleAnalysisIssue + { + public static RuleAnalysisResult UnsupportedRuleCall(SyntaxNode syntax, string value) + { + return Create(ValidationDiagnostics.UnsupportedRuleCall, syntax, value); + } + + public static RuleAnalysisResult UnsupportedSelector(SyntaxNode syntax, string value) + { + return Create(ValidationDiagnostics.UnsupportedSelector, syntax, value); + } + + public static RuleAnalysisResult UnsupportedArgument(SyntaxNode syntax, string value) + { + return Create(ValidationDiagnostics.UnsupportedArgument, syntax, value); + } + + public static RuleAnalysisResult InvalidCustomRule(SyntaxNode syntax, string value) + { + return Create(ValidationDiagnostics.InvalidCustomRule, syntax, value); + } + + private static RuleAnalysisResult Create( + DiagnosticDescriptor descriptor, + SyntaxNode syntax, + string value) + { + return RuleAnalysisResult.ForIssue(new ValidationIssue( + descriptor, + syntax.GetLocation(), + value)); + } + } +} diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs index d391bbe..5f39ad3 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs @@ -1,7 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using TinyValidations.SourceGen.Model; -using TinyValidations.SourceGen.Validation; namespace TinyValidations.SourceGen.Analysis.Rules { @@ -32,10 +31,7 @@ internal sealed class RuleInvocationAnalyzer var kind = _methodMap.GetKind(methodName); if (kind == null) { - return RuleAnalysisResult.ForIssue(new ValidationIssue( - ValidationDiagnostics.UnsupportedRuleCall, - memberAccess.Name.GetLocation(), - methodName)); + return RuleAnalysisIssue.UnsupportedRuleCall(memberAccess.Name, methodName); } if (kind.Value == RuleKind.Use) From 59bf5de53a766fab9c646205638b9ee93703820f Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:26:16 +0200 Subject: [PATCH 06/17] Extract validation rules invocation matcher --- .../Analysis/Rules/RuleInvocationAnalyzer.cs | 25 ++-------------- .../Rules/ValidationRulesInvocationMatcher.cs | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 23 deletions(-) create mode 100644 src/TinyValidations.SourceGen/Analysis/Rules/ValidationRulesInvocationMatcher.cs diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs index 5f39ad3..8013e04 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs @@ -6,6 +6,7 @@ namespace TinyValidations.SourceGen.Analysis.Rules { internal sealed class RuleInvocationAnalyzer { + private readonly ValidationRulesInvocationMatcher _invocationMatcher = new ValidationRulesInvocationMatcher(); private readonly RuleMethodMap _methodMap = new RuleMethodMap(); private readonly MemberRuleAnalyzer _memberRuleAnalyzer = new MemberRuleAnalyzer(); private readonly CustomRuleAnalyzer _customRuleAnalyzer = new CustomRuleAnalyzer(); @@ -22,7 +23,7 @@ internal sealed class RuleInvocationAnalyzer return null; } - if (!IsValidationRulesInvocation(semanticModel, memberAccess, invocation, validationRules)) + if (!_invocationMatcher.IsMatch(semanticModel, memberAccess, invocation, validationRules)) { return null; } @@ -46,27 +47,5 @@ internal sealed class RuleInvocationAnalyzer return _memberRuleAnalyzer.Analyze(kind.Value, invocation); } - - private static bool IsValidationRulesInvocation( - SemanticModel semanticModel, - MemberAccessExpressionSyntax memberAccess, - InvocationExpressionSyntax invocation, - INamedTypeSymbol validationRules) - { - var symbol = semanticModel.GetSymbolInfo(invocation).Symbol; - if (symbol is IMethodSymbol method) - { - return SymbolEqualityComparer.Default.Equals(method.ContainingType.OriginalDefinition, validationRules); - } - - var expressionType = semanticModel.GetTypeInfo(memberAccess.Expression).Type; - if (!(expressionType is INamedTypeSymbol namedType)) - { - return false; - } - - return SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, validationRules); - } - } } diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/ValidationRulesInvocationMatcher.cs b/src/TinyValidations.SourceGen/Analysis/Rules/ValidationRulesInvocationMatcher.cs new file mode 100644 index 0000000..3979e57 --- /dev/null +++ b/src/TinyValidations.SourceGen/Analysis/Rules/ValidationRulesInvocationMatcher.cs @@ -0,0 +1,29 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TinyValidations.SourceGen.Analysis.Rules +{ + internal sealed class ValidationRulesInvocationMatcher + { + public bool IsMatch( + SemanticModel semanticModel, + MemberAccessExpressionSyntax memberAccess, + InvocationExpressionSyntax invocation, + INamedTypeSymbol validationRules) + { + var symbol = semanticModel.GetSymbolInfo(invocation).Symbol; + if (symbol is IMethodSymbol method) + { + return SymbolEqualityComparer.Default.Equals(method.ContainingType.OriginalDefinition, validationRules); + } + + var expressionType = semanticModel.GetTypeInfo(memberAccess.Expression).Type; + if (!(expressionType is INamedTypeSymbol namedType)) + { + return false; + } + + return SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, validationRules); + } + } +} From ed16e1cdcdcb47bdfaf46f97b683d32ed3c087ff Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:28:35 +0200 Subject: [PATCH 07/17] Clarify rule analyzer arguments --- .../Analysis/Rules/MemberRuleAnalyzer.cs | 7 +++--- .../Analysis/Rules/RequiresRuleAnalyzer.cs | 22 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs index 32c8391..0724157 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs @@ -15,12 +15,13 @@ public RuleAnalysisResult Analyze(RuleKind kind, InvocationExpressionSyntax invo return RuleAnalysisIssue.UnsupportedSelector(invocation, invocation.ToString()); } - var member = _memberAccessAnalyzer.Analyze(invocation.ArgumentList.Arguments[0].Expression); + var selectorArgument = invocation.ArgumentList.Arguments[0]; + var member = _memberAccessAnalyzer.Analyze(selectorArgument.Expression); if (member == null) { return RuleAnalysisIssue.UnsupportedSelector( - invocation.ArgumentList.Arguments[0], - invocation.ArgumentList.Arguments[0].Expression.ToString()); + selectorArgument, + selectorArgument.Expression.ToString()); } if (HasUnsupportedArgument(kind, invocation)) diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs index 7d5100f..eb40d9b 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs @@ -17,29 +17,33 @@ public RuleAnalysisResult Analyze( return RuleAnalysisIssue.UnsupportedArgument(invocation, invocation.ToString()); } - var member = _memberAccessAnalyzer.Analyze(invocation.ArgumentList.Arguments[0].Expression); + var selectorArgument = invocation.ArgumentList.Arguments[0]; + var requirementArgument = invocation.ArgumentList.Arguments[1]; + var messageArgument = invocation.ArgumentList.Arguments[2]; + + var member = _memberAccessAnalyzer.Analyze(selectorArgument.Expression); if (member == null) { return RuleAnalysisIssue.UnsupportedSelector( - invocation.ArgumentList.Arguments[0], - invocation.ArgumentList.Arguments[0].Expression.ToString()); + selectorArgument, + selectorArgument.Expression.ToString()); } - if (!IsSupportedRequirementMethod(semanticModel, invocation.ArgumentList.Arguments[1].Expression, out var requirementMethod)) + if (!IsSupportedRequirementMethod(semanticModel, requirementArgument.Expression, out var requirementMethod)) { return RuleAnalysisIssue.UnsupportedArgument( - invocation.ArgumentList.Arguments[1], - invocation.ArgumentList.Arguments[1].Expression.ToString()); + requirementArgument, + requirementArgument.Expression.ToString()); } if (!IsSupportedArgument(invocation, 2)) { return RuleAnalysisIssue.UnsupportedArgument( - invocation.ArgumentList.Arguments[2], - invocation.ArgumentList.Arguments[2].Expression.ToString()); + messageArgument, + messageArgument.Expression.ToString()); } - var message = invocation.ArgumentList.Arguments[2].Expression.ToString(); + var message = messageArgument.Expression.ToString(); return RuleAnalysisResult.ForRule(new RuleDefinition( RuleKind.Requires, From eca76c7f129a53ca66fa8f765cac61632189772e Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:31:52 +0200 Subject: [PATCH 08/17] Clarify member rule analyzer flow --- .../Analysis/Rules/MemberRuleAnalyzer.cs | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs index 0724157..9e6e149 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs @@ -10,13 +10,13 @@ internal sealed class MemberRuleAnalyzer public RuleAnalysisResult Analyze(RuleKind kind, InvocationExpressionSyntax invocation) { - if (invocation.ArgumentList.Arguments.Count == 0) + if (!HasSelector(invocation)) { return RuleAnalysisIssue.UnsupportedSelector(invocation, invocation.ToString()); } var selectorArgument = invocation.ArgumentList.Arguments[0]; - var member = _memberAccessAnalyzer.Analyze(selectorArgument.Expression); + var member = AnalyzeSelector(selectorArgument); if (member == null) { return RuleAnalysisIssue.UnsupportedSelector( @@ -29,6 +29,14 @@ public RuleAnalysisResult Analyze(RuleKind kind, InvocationExpressionSyntax invo return RuleAnalysisIssue.UnsupportedArgument(invocation, invocation.ToString()); } + return CreateRule(kind, invocation, member); + } + + private RuleAnalysisResult CreateRule( + RuleKind kind, + InvocationExpressionSyntax invocation, + AnalyzedMemberAccess member) + { var argument = _argumentAnalyzer.GetRuleArgument(kind, invocation); var message = _argumentAnalyzer.GetMessage(kind, invocation); @@ -41,27 +49,46 @@ public RuleAnalysisResult Analyze(RuleKind kind, InvocationExpressionSyntax invo string.Empty)); } + private AnalyzedMemberAccess? AnalyzeSelector(ArgumentSyntax selectorArgument) + { + return _memberAccessAnalyzer.Analyze(selectorArgument.Expression); + } + + private static bool HasSelector(InvocationExpressionSyntax invocation) + { + return invocation.ArgumentList.Arguments.Count > 0; + } + private static bool HasUnsupportedArgument(RuleKind kind, InvocationExpressionSyntax invocation) + { + if (HasUnsupportedValueArgument(kind, invocation)) + { + return true; + } + + return HasUnsupportedMessageArgument(kind, invocation); + } + + private static bool HasUnsupportedValueArgument(RuleKind kind, InvocationExpressionSyntax invocation) { var valueArgumentIndex = RuleShape.ValueArgumentIndex(kind); - if (valueArgumentIndex >= 0) + if (valueArgumentIndex < 0) { - if (!IsSupportedArgument(invocation, valueArgumentIndex)) - { - return true; - } + return false; } + return !IsSupportedArgument(invocation, valueArgumentIndex); + } + + private static bool HasUnsupportedMessageArgument(RuleKind kind, InvocationExpressionSyntax invocation) + { var messageArgumentIndex = RuleShape.MessageArgumentIndex(kind); - if (HasArgument(invocation, messageArgumentIndex)) + if (!HasArgument(invocation, messageArgumentIndex)) { - if (!IsSupportedArgument(invocation, messageArgumentIndex)) - { - return true; - } + return false; } - return false; + return !IsSupportedArgument(invocation, messageArgumentIndex); } private static bool IsSupportedArgument(InvocationExpressionSyntax invocation, int argumentIndex) From 17419c3462fcf3abad703a19ee6ed288b0e08257 Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:33:14 +0200 Subject: [PATCH 09/17] Clarify requires rule analyzer flow --- .../Analysis/Rules/RequiresRuleAnalyzer.cs | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs index eb40d9b..a9c79c3 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs @@ -21,7 +21,7 @@ public RuleAnalysisResult Analyze( var requirementArgument = invocation.ArgumentList.Arguments[1]; var messageArgument = invocation.ArgumentList.Arguments[2]; - var member = _memberAccessAnalyzer.Analyze(selectorArgument.Expression); + var member = AnalyzeSelector(selectorArgument); if (member == null) { return RuleAnalysisIssue.UnsupportedSelector( @@ -36,13 +36,21 @@ public RuleAnalysisResult Analyze( requirementArgument.Expression.ToString()); } - if (!IsSupportedArgument(invocation, 2)) + if (!IsSupportedMessage(messageArgument)) { return RuleAnalysisIssue.UnsupportedArgument( messageArgument, messageArgument.Expression.ToString()); } + return CreateRule(member, requirementMethod, messageArgument); + } + + private RuleAnalysisResult CreateRule( + AnalyzedMemberAccess member, + string requirementMethod, + ArgumentSyntax messageArgument) + { var message = messageArgument.Expression.ToString(); return RuleAnalysisResult.ForRule(new RuleDefinition( @@ -55,19 +63,19 @@ public RuleAnalysisResult Analyze( requirementMethod)); } - private static bool IsSupportedArgument(InvocationExpressionSyntax invocation, int argumentIndex) + private AnalyzedMemberAccess? AnalyzeSelector(ArgumentSyntax selectorArgument) + { + return _memberAccessAnalyzer.Analyze(selectorArgument.Expression); + } + + private static bool IsSupportedMessage(ArgumentSyntax messageArgument) { - if (!HasArgument(invocation, argumentIndex)) + if (!(messageArgument.Expression is LiteralExpressionSyntax)) { return false; } - return invocation.ArgumentList.Arguments[argumentIndex].Expression is LiteralExpressionSyntax; - } - - private static bool HasArgument(InvocationExpressionSyntax invocation, int argumentIndex) - { - return invocation.ArgumentList.Arguments.Count > argumentIndex; + return true; } private static bool IsSupportedRequirementMethod( From 402dc2ef4e1eb59cb671283b39235560d48f2693 Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:34:37 +0200 Subject: [PATCH 10/17] Clarify custom rule analyzer flow --- .../Analysis/Rules/CustomRuleAnalyzer.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs index 037cdf6..cab20f6 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs @@ -29,7 +29,11 @@ public RuleAnalysisResult Analyze( } var customRuleType = GetTypeName(typeSyntax, typeSymbol); + return CreateRule(customRuleType); + } + private static RuleAnalysisResult CreateRule(string customRuleType) + { return RuleAnalysisResult.ForRule(new RuleDefinition( RuleKind.Use, string.Empty, @@ -74,22 +78,32 @@ private static bool IsValidCustomRule(ITypeSymbol? typeSymbol, INamedTypeSymbol private static bool IsAsyncValidationRule(INamedTypeSymbol candidate, INamedTypeSymbol commandType) { - if (candidate.ContainingNamespace.ToDisplayString() != "TinyValidations") + if (!IsTinyValidationsRule(candidate)) { return false; } - if (candidate.Name != "IAsyncValidationRule") + if (!HasSingleTypeArgument(candidate)) { return false; } - if (candidate.TypeArguments.Length != 1) + return SymbolEqualityComparer.Default.Equals(candidate.TypeArguments[0], commandType); + } + + private static bool IsTinyValidationsRule(INamedTypeSymbol candidate) + { + if (candidate.ContainingNamespace.ToDisplayString() != "TinyValidations") { return false; } - return SymbolEqualityComparer.Default.Equals(candidate.TypeArguments[0], commandType); + return candidate.Name == "IAsyncValidationRule"; + } + + private static bool HasSingleTypeArgument(INamedTypeSymbol candidate) + { + return candidate.TypeArguments.Length == 1; } } } From 95f7fc6e7415f6dfb03d00ca83bdaeed18e984a9 Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:35:55 +0200 Subject: [PATCH 11/17] Clarify validation rules invocation matcher --- .../Rules/ValidationRulesInvocationMatcher.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/ValidationRulesInvocationMatcher.cs b/src/TinyValidations.SourceGen/Analysis/Rules/ValidationRulesInvocationMatcher.cs index 3979e57..aee5445 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/ValidationRulesInvocationMatcher.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/ValidationRulesInvocationMatcher.cs @@ -10,13 +10,34 @@ public bool IsMatch( MemberAccessExpressionSyntax memberAccess, InvocationExpressionSyntax invocation, INamedTypeSymbol validationRules) + { + if (MatchesResolvedMethod(semanticModel, invocation, validationRules)) + { + return true; + } + + return MatchesMemberAccessExpression(semanticModel, memberAccess, validationRules); + } + + private static bool MatchesResolvedMethod( + SemanticModel semanticModel, + InvocationExpressionSyntax invocation, + INamedTypeSymbol validationRules) { var symbol = semanticModel.GetSymbolInfo(invocation).Symbol; - if (symbol is IMethodSymbol method) + if (!(symbol is IMethodSymbol method)) { - return SymbolEqualityComparer.Default.Equals(method.ContainingType.OriginalDefinition, validationRules); + return false; } + return SymbolEqualityComparer.Default.Equals(method.ContainingType.OriginalDefinition, validationRules); + } + + private static bool MatchesMemberAccessExpression( + SemanticModel semanticModel, + MemberAccessExpressionSyntax memberAccess, + INamedTypeSymbol validationRules) + { var expressionType = semanticModel.GetTypeInfo(memberAccess.Expression).Type; if (!(expressionType is INamedTypeSymbol namedType)) { From 381a97c5509471000647dec402739f1358c68057 Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:37:28 +0200 Subject: [PATCH 12/17] Clarify rule invocation routing --- .../Analysis/Rules/RuleInvocationAnalyzer.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs index 8013e04..f6284e4 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs @@ -35,17 +35,32 @@ internal sealed class RuleInvocationAnalyzer return RuleAnalysisIssue.UnsupportedRuleCall(memberAccess.Name, methodName); } - if (kind.Value == RuleKind.Use) + return AnalyzeKnownRule( + semanticModel, + invocation, + memberAccess, + commandType, + kind.Value); + } + + private RuleAnalysisResult AnalyzeKnownRule( + SemanticModel semanticModel, + InvocationExpressionSyntax invocation, + MemberAccessExpressionSyntax memberAccess, + INamedTypeSymbol commandType, + RuleKind ruleKind) + { + if (ruleKind == RuleKind.Use) { return _customRuleAnalyzer.Analyze(semanticModel, memberAccess.Name, commandType); } - if (kind.Value == RuleKind.Requires) + if (ruleKind == RuleKind.Requires) { return _requiresRuleAnalyzer.Analyze(semanticModel, invocation); } - return _memberRuleAnalyzer.Analyze(kind.Value, invocation); + return _memberRuleAnalyzer.Analyze(ruleKind, invocation); } } } From 97782a760e31041f3f4e6e89458b2598e24e7878 Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:40:15 +0200 Subject: [PATCH 13/17] Rename rule analysis folder --- .../Analysis/Declarations/DefineMethodAnalyzer.cs | 2 +- .../Analysis/{Rules => RuleInvocations}/AnalyzedMemberAccess.cs | 2 +- .../Analysis/{Rules => RuleInvocations}/CustomRuleAnalyzer.cs | 2 +- .../Analysis/{Rules => RuleInvocations}/MemberAccessAnalyzer.cs | 2 +- .../Analysis/{Rules => RuleInvocations}/MemberRuleAnalyzer.cs | 2 +- .../Analysis/{Rules => RuleInvocations}/RequiresRuleAnalyzer.cs | 2 +- .../Analysis/{Rules => RuleInvocations}/RuleAnalysisIssue.cs | 2 +- .../Analysis/{Rules => RuleInvocations}/RuleAnalysisResult.cs | 2 +- .../Analysis/{Rules => RuleInvocations}/RuleArgumentAnalyzer.cs | 2 +- .../{Rules => RuleInvocations}/RuleInvocationAnalyzer.cs | 2 +- .../Analysis/{Rules => RuleInvocations}/RuleMethodMap.cs | 2 +- .../Analysis/{Rules => RuleInvocations}/RuleShape.cs | 2 +- .../ValidationRulesInvocationMatcher.cs | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) rename src/TinyValidations.SourceGen/Analysis/{Rules => RuleInvocations}/AnalyzedMemberAccess.cs (81%) rename src/TinyValidations.SourceGen/Analysis/{Rules => RuleInvocations}/CustomRuleAnalyzer.cs (98%) rename src/TinyValidations.SourceGen/Analysis/{Rules => RuleInvocations}/MemberAccessAnalyzer.cs (98%) rename src/TinyValidations.SourceGen/Analysis/{Rules => RuleInvocations}/MemberRuleAnalyzer.cs (98%) rename src/TinyValidations.SourceGen/Analysis/{Rules => RuleInvocations}/RequiresRuleAnalyzer.cs (98%) rename src/TinyValidations.SourceGen/Analysis/{Rules => RuleInvocations}/RuleAnalysisIssue.cs (95%) rename src/TinyValidations.SourceGen/Analysis/{Rules => RuleInvocations}/RuleAnalysisResult.cs (91%) rename src/TinyValidations.SourceGen/Analysis/{Rules => RuleInvocations}/RuleArgumentAnalyzer.cs (95%) rename src/TinyValidations.SourceGen/Analysis/{Rules => RuleInvocations}/RuleInvocationAnalyzer.cs (97%) rename src/TinyValidations.SourceGen/Analysis/{Rules => RuleInvocations}/RuleMethodMap.cs (94%) rename src/TinyValidations.SourceGen/Analysis/{Rules => RuleInvocations}/RuleShape.cs (93%) rename src/TinyValidations.SourceGen/Analysis/{Rules => RuleInvocations}/ValidationRulesInvocationMatcher.cs (96%) diff --git a/src/TinyValidations.SourceGen/Analysis/Declarations/DefineMethodAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/Declarations/DefineMethodAnalyzer.cs index b5ed211..f322fd2 100644 --- a/src/TinyValidations.SourceGen/Analysis/Declarations/DefineMethodAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/Declarations/DefineMethodAnalyzer.cs @@ -2,7 +2,7 @@ using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using TinyValidations.SourceGen.Analysis.Rules; +using TinyValidations.SourceGen.Analysis.RuleInvocations; using TinyValidations.SourceGen.Model; namespace TinyValidations.SourceGen.Analysis.Declarations diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/AnalyzedMemberAccess.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/AnalyzedMemberAccess.cs similarity index 81% rename from src/TinyValidations.SourceGen/Analysis/Rules/AnalyzedMemberAccess.cs rename to src/TinyValidations.SourceGen/Analysis/RuleInvocations/AnalyzedMemberAccess.cs index 72cf99b..de0ce42 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/AnalyzedMemberAccess.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/AnalyzedMemberAccess.cs @@ -1,4 +1,4 @@ -namespace TinyValidations.SourceGen.Analysis.Rules +namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal sealed class AnalyzedMemberAccess { diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/CustomRuleAnalyzer.cs similarity index 98% rename from src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs rename to src/TinyValidations.SourceGen/Analysis/RuleInvocations/CustomRuleAnalyzer.cs index cab20f6..1957876 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/CustomRuleAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/CustomRuleAnalyzer.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using TinyValidations.SourceGen.Model; -namespace TinyValidations.SourceGen.Analysis.Rules +namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal sealed class CustomRuleAnalyzer { diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/MemberAccessAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/MemberAccessAnalyzer.cs similarity index 98% rename from src/TinyValidations.SourceGen/Analysis/Rules/MemberAccessAnalyzer.cs rename to src/TinyValidations.SourceGen/Analysis/RuleInvocations/MemberAccessAnalyzer.cs index 87a5982..1f71b45 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/MemberAccessAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/MemberAccessAnalyzer.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace TinyValidations.SourceGen.Analysis.Rules +namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal sealed class MemberAccessAnalyzer { diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/MemberRuleAnalyzer.cs similarity index 98% rename from src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs rename to src/TinyValidations.SourceGen/Analysis/RuleInvocations/MemberRuleAnalyzer.cs index 9e6e149..eeb9d33 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/MemberRuleAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/MemberRuleAnalyzer.cs @@ -1,7 +1,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using TinyValidations.SourceGen.Model; -namespace TinyValidations.SourceGen.Analysis.Rules +namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal sealed class MemberRuleAnalyzer { diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RequiresRuleAnalyzer.cs similarity index 98% rename from src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs rename to src/TinyValidations.SourceGen/Analysis/RuleInvocations/RequiresRuleAnalyzer.cs index a9c79c3..6260e1d 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RequiresRuleAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RequiresRuleAnalyzer.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using TinyValidations.SourceGen.Model; -namespace TinyValidations.SourceGen.Analysis.Rules +namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal sealed class RequiresRuleAnalyzer { diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleAnalysisIssue.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleAnalysisIssue.cs similarity index 95% rename from src/TinyValidations.SourceGen/Analysis/Rules/RuleAnalysisIssue.cs rename to src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleAnalysisIssue.cs index c02c4c7..80aeb5b 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleAnalysisIssue.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleAnalysisIssue.cs @@ -2,7 +2,7 @@ using TinyValidations.SourceGen.Model; using TinyValidations.SourceGen.Validation; -namespace TinyValidations.SourceGen.Analysis.Rules +namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal static class RuleAnalysisIssue { diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleAnalysisResult.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleAnalysisResult.cs similarity index 91% rename from src/TinyValidations.SourceGen/Analysis/Rules/RuleAnalysisResult.cs rename to src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleAnalysisResult.cs index e04c614..64ebbf6 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleAnalysisResult.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleAnalysisResult.cs @@ -1,6 +1,6 @@ using TinyValidations.SourceGen.Model; -namespace TinyValidations.SourceGen.Analysis.Rules +namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal sealed class RuleAnalysisResult { diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleArgumentAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleArgumentAnalyzer.cs similarity index 95% rename from src/TinyValidations.SourceGen/Analysis/Rules/RuleArgumentAnalyzer.cs rename to src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleArgumentAnalyzer.cs index f78bd8a..85ccde2 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleArgumentAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleArgumentAnalyzer.cs @@ -1,7 +1,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using TinyValidations.SourceGen.Model; -namespace TinyValidations.SourceGen.Analysis.Rules +namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal sealed class RuleArgumentAnalyzer { diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleInvocationAnalyzer.cs similarity index 97% rename from src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs rename to src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleInvocationAnalyzer.cs index f6284e4..0ae29d3 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleInvocationAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleInvocationAnalyzer.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using TinyValidations.SourceGen.Model; -namespace TinyValidations.SourceGen.Analysis.Rules +namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal sealed class RuleInvocationAnalyzer { diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleMethodMap.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleMethodMap.cs similarity index 94% rename from src/TinyValidations.SourceGen/Analysis/Rules/RuleMethodMap.cs rename to src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleMethodMap.cs index 0d14d7e..7a076d2 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleMethodMap.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleMethodMap.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using TinyValidations.SourceGen.Model; -namespace TinyValidations.SourceGen.Analysis.Rules +namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal sealed class RuleMethodMap { diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/RuleShape.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleShape.cs similarity index 93% rename from src/TinyValidations.SourceGen/Analysis/Rules/RuleShape.cs rename to src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleShape.cs index f51fa83..14cf6d2 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/RuleShape.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/RuleShape.cs @@ -1,6 +1,6 @@ using TinyValidations.SourceGen.Model; -namespace TinyValidations.SourceGen.Analysis.Rules +namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal static class RuleShape { diff --git a/src/TinyValidations.SourceGen/Analysis/Rules/ValidationRulesInvocationMatcher.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/ValidationRulesInvocationMatcher.cs similarity index 96% rename from src/TinyValidations.SourceGen/Analysis/Rules/ValidationRulesInvocationMatcher.cs rename to src/TinyValidations.SourceGen/Analysis/RuleInvocations/ValidationRulesInvocationMatcher.cs index aee5445..1b97767 100644 --- a/src/TinyValidations.SourceGen/Analysis/Rules/ValidationRulesInvocationMatcher.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/ValidationRulesInvocationMatcher.cs @@ -1,7 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace TinyValidations.SourceGen.Analysis.Rules +namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal sealed class ValidationRulesInvocationMatcher { From 09f68061ccebc327353afb62c6c042345c9e25d0 Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:43:03 +0200 Subject: [PATCH 14/17] Simplify member access analyzer type names --- .../Analysis/RuleInvocations/MemberAccessAnalyzer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/TinyValidations.SourceGen/Analysis/RuleInvocations/MemberAccessAnalyzer.cs b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/MemberAccessAnalyzer.cs index 1f71b45..71ce931 100644 --- a/src/TinyValidations.SourceGen/Analysis/RuleInvocations/MemberAccessAnalyzer.cs +++ b/src/TinyValidations.SourceGen/Analysis/RuleInvocations/MemberAccessAnalyzer.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace TinyValidations.SourceGen.Analysis.RuleInvocations { internal sealed class MemberAccessAnalyzer { - public AnalyzedMemberAccess? Analyze(Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax expression) + public AnalyzedMemberAccess? Analyze(ExpressionSyntax expression) { if (!(expression is LambdaExpressionSyntax lambda)) { @@ -58,10 +59,10 @@ private static bool HasSingleParameter(ParenthesizedLambdaExpressionSyntax lambd return lambda.ParameterList.Parameters.Count == 1; } - private static List ReadMembers(Microsoft.CodeAnalysis.SyntaxNode body, string parameterName) + private static List ReadMembers(SyntaxNode body, string parameterName) { var members = new List(); - ExpressionSyntax? current = body as Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax; + ExpressionSyntax? current = body as ExpressionSyntax; while (current is MemberAccessExpressionSyntax memberAccess) { From 48cf290b3c28a6909ef98c2963d9d87064ac670d Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:46:13 +0200 Subject: [PATCH 15/17] Add built-in rule success behavior tests --- .../ValidationBehaviorTests.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/TinyValidations.Tests/ValidationBehaviorTests.cs b/tests/TinyValidations.Tests/ValidationBehaviorTests.cs index 2c38342..414f836 100644 --- a/tests/TinyValidations.Tests/ValidationBehaviorTests.cs +++ b/tests/TinyValidations.Tests/ValidationBehaviorTests.cs @@ -26,6 +26,23 @@ public async Task Built_in_rules_return_validation_errors() AssertHasError(result, nameof(CreateProfile.Roles), "Roles must contain at least one item."); } + [Fact] + public async Task Built_in_rules_return_valid_result_when_values_satisfy_rules() + { + var validator = BuildValidator(); + var command = new CreateProfile( + "person@example.com", + "Valid Name", + 18, + "ABC", + new[] { "admin" }); + + var result = await validator.ValidateAsync(command); + + Assert.True(result.IsValid); + Assert.Empty(result.Errors); + } + [Fact] public async Task Built_in_rules_use_custom_messages() { @@ -70,6 +87,24 @@ public async Task Remaining_built_in_rules_return_validation_errors() AssertHasError(result, nameof(ConfigureProduct.Rating), "Rating must be at most 5."); } + [Fact] + public async Task Remaining_built_in_rules_accept_boundary_values() + { + var validator = BuildValidator(); + var command = new ConfigureProduct( + "Product", + "Category", + "ABC", + 1, + 9, + 5); + + var result = await validator.ValidateAsync(command); + + Assert.True(result.IsValid); + Assert.Empty(result.Errors); + } + [Fact] public async Task Custom_rules_are_resolved_from_dependency_injection() { From 0d3a68198606f5b71b4453714c610d418a550d5a Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:47:32 +0200 Subject: [PATCH 16/17] Add text rule behavior tests --- .../ValidationBehaviorTests.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/TinyValidations.Tests/ValidationBehaviorTests.cs b/tests/TinyValidations.Tests/ValidationBehaviorTests.cs index 414f836..d039d1e 100644 --- a/tests/TinyValidations.Tests/ValidationBehaviorTests.cs +++ b/tests/TinyValidations.Tests/ValidationBehaviorTests.cs @@ -105,6 +105,32 @@ public async Task Remaining_built_in_rules_accept_boundary_values() Assert.Empty(result.Errors); } + [Fact] + public async Task Text_rules_reject_null_empty_and_whitespace_values() + { + var validator = BuildValidator(); + var command = new UpdateContact(null, string.Empty, " "); + + var result = await validator.ValidateAsync(command); + + Assert.False(result.IsValid); + AssertHasError(result, nameof(UpdateContact.RequiredEmail), "RequiredEmail is required."); + AssertHasError(result, nameof(UpdateContact.DisplayName), "DisplayName must contain text."); + AssertHasError(result, nameof(UpdateContact.Notes), "Notes must contain text."); + } + + [Fact] + public async Task Text_rules_accept_non_empty_text_values() + { + var validator = BuildValidator(); + var command = new UpdateContact("person@example.com", "Person", "Available"); + + var result = await validator.ValidateAsync(command); + + Assert.True(result.IsValid); + Assert.Empty(result.Errors); + } + [Fact] public async Task Custom_rules_are_resolved_from_dependency_injection() { @@ -308,6 +334,22 @@ public void Define(ValidationRules rules) } } +public sealed record UpdateContact( + string? RequiredEmail, + string DisplayName, + string Notes); + +public sealed class UpdateContactValidation : IValidation +{ + public void Define(ValidationRules rules) + { + rules.Required(x => x.RequiredEmail); + rules.HasText(x => x.DisplayName); + rules.HasText(x => x.Notes); + rules.Email(x => x.RequiredEmail); + } +} + public sealed record CreateProfile( string Email, string DisplayName, From 7f624e04eac8a0944da3d91cb563513df0b785f1 Mon Sep 17 00:00:00 2001 From: George Date: Sat, 30 May 2026 17:49:10 +0200 Subject: [PATCH 17/17] Add generator define discovery tests --- .../DiscoveryTests.cs | 34 +++++++++++++++++++ .../GenerationTests.cs | 30 ++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/tests/TinyValidations.SourceGen.Tests/DiscoveryTests.cs b/tests/TinyValidations.SourceGen.Tests/DiscoveryTests.cs index 26b8fc3..2289fd8 100644 --- a/tests/TinyValidations.SourceGen.Tests/DiscoveryTests.cs +++ b/tests/TinyValidations.SourceGen.Tests/DiscoveryTests.cs @@ -62,4 +62,38 @@ public sealed class CreateUser Assert.DoesNotContain("Email is required.", text); } + + [Fact] + public void Ignores_validation_rule_calls_outside_define_method() + { + var source = """ +using TinyValidations; + +public sealed class CreateUserValidation : IValidation +{ + public void Define(ValidationRules rules) + { + rules.Required(x => x.Email); + } + + public void Configure(ValidationRules rules) + { + rules.Required(x => x.DisplayName); + } +} + +public sealed class CreateUser +{ + public string? Email { get; init; } + public string? DisplayName { get; init; } +} +"""; + + var result = SourceGeneratorTestHost.Run(source); + var text = result.SingleGeneratedSource(); + + result.ShouldHaveNoDiagnostics(); + Assert.Contains("Email is required.", text); + Assert.DoesNotContain("DisplayName is required.", text); + } } diff --git a/tests/TinyValidations.SourceGen.Tests/GenerationTests.cs b/tests/TinyValidations.SourceGen.Tests/GenerationTests.cs index fb01a1c..b064e14 100644 --- a/tests/TinyValidations.SourceGen.Tests/GenerationTests.cs +++ b/tests/TinyValidations.SourceGen.Tests/GenerationTests.cs @@ -79,6 +79,36 @@ public sealed class CreateUser Assert.Contains("Email is required.", text); } + [Fact] + public void Generates_all_rules_from_explicit_define_implementation() + { + var source = """ +using TinyValidations; + +public sealed class CreateUserValidation : IValidation +{ + void IValidation.Define(ValidationRules rules) + { + rules.Required(x => x.Email); + rules.TextLengthAtLeast(x => x.DisplayName, 2); + } +} + +public sealed class CreateUser +{ + public string? Email { get; init; } + public string? DisplayName { get; init; } +} +"""; + + var result = SourceGeneratorTestHost.Run(source); + var text = result.SingleGeneratedSource(); + + result.ShouldHaveNoDiagnostics(); + Assert.Contains("Email is required.", text); + Assert.Contains("DisplayName must contain at least 2 characters.", text); + } + [Fact] public void Generates_static_requires_rule_call() {