-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: split oversized files under 500-LOC threshold #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c7f0948
c720fac
d85952e
71a1625
f6e0e9c
d483857
12a1a6f
30626ea
20852a5
927d59d
c019e16
518fe8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| using ErrorOr.Generators; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
|
|
||
| namespace ErrorOr.Analyzers; | ||
|
|
||
| /// <summary> | ||
| /// Body-source counting (EOE006) and DataAnnotations reflection check (EOE039) for the | ||
| /// <see cref="ErrorOrEndpointAnalyzer"/>. Body classification routes through | ||
| /// <see cref="ErrorOrContext"/> so analyzer and generator share one source of truth | ||
| /// (FQN + inheritance, not name-substring match). | ||
| /// </summary> | ||
| public sealed partial class ErrorOrEndpointAnalyzer | ||
| { | ||
| private static int CountBodySources(IMethodSymbol method) | ||
| { | ||
| var bodyCount = 0; | ||
| var hasFromForm = false; | ||
| var hasStream = false; | ||
|
|
||
| foreach (var param in method.Parameters) | ||
| { | ||
| // Check for body-related attributes using HasAttribute | ||
| if (param.HasAttribute(WellKnownTypes.FromBodyAttribute)) | ||
| { | ||
| bodyCount++; | ||
| continue; | ||
| } | ||
|
|
||
| if (param.HasAttribute(WellKnownTypes.FromFormAttribute)) | ||
| { | ||
| hasFromForm = true; | ||
| continue; | ||
| } | ||
|
|
||
| // Body-related types: route through ErrorOrContext so analyzer + generator | ||
| // share one source of truth (FQN + inheritance, not name substring match). | ||
| if (ErrorOrContext.IsStream(param.Type) || ErrorOrContext.IsPipeReader(param.Type)) | ||
| hasStream = true; | ||
| else if (ErrorOrContext.IsFormFile(param.Type) || | ||
| ErrorOrContext.IsFormFileCollection(param.Type) || | ||
| ErrorOrContext.IsFormCollection(param.Type)) | ||
| hasFromForm = true; | ||
| } | ||
|
|
||
| // Multiple [FromBody] is always an error | ||
| if (bodyCount > 1) return bodyCount; | ||
|
|
||
| // Otherwise return number of distinct body source buckets used | ||
| return (bodyCount > 0 ? 1 : 0) + (hasFromForm ? 1 : 0) + (hasStream ? 1 : 0); | ||
| } | ||
|
|
||
| private static bool HasAcceptedResponseAttribute(ISymbol method) | ||
| { | ||
| return method.HasAttribute(WellKnownTypes.AcceptedResponseAttribute); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if any parameter has validation attributes from System.ComponentModel.DataAnnotations. | ||
| /// Validator.TryValidateObject uses reflection internally. | ||
| /// </summary> | ||
| private static void CheckForValidationAttributes( | ||
| in SymbolAnalysisContext context, | ||
| IMethodSymbol method) | ||
| { | ||
| var validationAttributeType = context.Compilation.GetTypeByMetadataName(WellKnownTypes.ValidationAttribute); | ||
| if (validationAttributeType is null) return; | ||
|
|
||
| foreach (var param in method.Parameters) | ||
| { | ||
| foreach (var attr in param.GetAttributes()) | ||
| { | ||
| if (attr.AttributeClass is null) continue; | ||
|
|
||
| // Check if the attribute inherits from ValidationAttribute | ||
| if (InheritsFrom(attr.AttributeClass, validationAttributeType)) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create( | ||
| Descriptors.ValidationUsesReflection, | ||
| param.Locations.FirstOrDefault() ?? method.Locations.FirstOrDefault(), | ||
| param.Name, | ||
| method.Name)); | ||
| break; // Only report once per parameter | ||
| } | ||
| } | ||
|
Comment on lines
+69
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. EOE039 misses validation attributes on request DTO properties. This only inspects 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if a type inherits from a base type. | ||
| /// </summary> | ||
| private static bool InheritsFrom(ITypeSymbol type, ISymbol baseType) | ||
| { | ||
| var current = type.BaseType; | ||
| while (current is not null) | ||
| { | ||
| if (SymbolEqualityComparer.Default.Equals(current, baseType)) return true; | ||
|
|
||
| current = current.BaseType; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| using ANcpLua.Roslyn.Utilities; | ||
| using ErrorOr.Generators; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
|
|
||
| namespace ErrorOr.Analyzers; | ||
|
|
||
| /// <summary> | ||
| /// Route pattern validation and route-constraint type checking. Hosts <c>EOE005</c> | ||
| /// (pattern syntax) and <c>EOE020</c> (constraint vs. CLR-type mismatch) for the | ||
| /// <see cref="ErrorOrEndpointAnalyzer"/>. | ||
| /// </summary> | ||
| public sealed partial class ErrorOrEndpointAnalyzer | ||
| { | ||
| /// <summary> | ||
| /// Validates route constraint types match method parameter types (EOE020). | ||
| /// </summary> | ||
| private static void ValidateConstraintTypes( | ||
| in SymbolAnalysisContext context, | ||
| ImmutableArray<RouteParameterInfo> routeParams, | ||
| IReadOnlyDictionary<string, RouteMethodParameterInfo> methodParamsByRouteName, | ||
| Location attributeLocation) | ||
| { | ||
| foreach (var rp in routeParams) | ||
| ValidateSingleRouteConstraint(in context, rp, methodParamsByRouteName, attributeLocation); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Validates a single route parameter constraint against its bound method parameter. | ||
| /// </summary> | ||
| private static void ValidateSingleRouteConstraint( | ||
| in SymbolAnalysisContext context, | ||
| RouteParameterInfo rp, | ||
| IReadOnlyDictionary<string, RouteMethodParameterInfo> methodParamsByRouteName, | ||
| Location attributeLocation) | ||
| { | ||
| // Skip if no constraint or not bound to a method parameter | ||
| if (rp.Constraint is not { } constraint || | ||
| !methodParamsByRouteName.TryGetValue(rp.Name, out var mp)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (mp.TypeFqn is not { } typeFqn) return; | ||
|
|
||
| // Skip format-only constraints | ||
| if (IsFormatOnlyConstraint(constraint)) return; | ||
|
|
||
| // Validate based on constraint type | ||
| if (rp.IsCatchAll) | ||
| ValidateCatchAllConstraint(in context, rp, mp, typeFqn, attributeLocation); | ||
| else | ||
| ValidateTypedConstraint(in context, rp, constraint, mp, typeFqn, attributeLocation); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if a constraint is format-only and doesn't constrain the CLR type. | ||
| /// Delegates to shared RouteValidator to avoid duplication. | ||
| /// </summary> | ||
| private static bool IsFormatOnlyConstraint(string constraint) | ||
| { | ||
| return RouteValidator.FormatOnlyConstraints.Contains(constraint); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Validates that a catch-all parameter is bound to a string type. | ||
| /// </summary> | ||
| private static void ValidateCatchAllConstraint( | ||
| in SymbolAnalysisContext context, | ||
| RouteParameterInfo rp, | ||
| RouteMethodParameterInfo mp, | ||
| string typeFqn, | ||
| Location attributeLocation) | ||
| { | ||
| if (!IsStringType(typeFqn)) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create( | ||
| Descriptors.RouteConstraintTypeMismatch, | ||
| attributeLocation, | ||
| rp.Name, | ||
| "*", | ||
| "string", | ||
| mp.Name, | ||
| NormalizeTypeName(typeFqn))); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Validates that a typed constraint matches the bound parameter type. | ||
| /// Uses shared RouteValidator.ConstraintToTypes to avoid duplication. | ||
| /// </summary> | ||
| private static void ValidateTypedConstraint( | ||
| in SymbolAnalysisContext context, | ||
| RouteParameterInfo rp, | ||
| string constraint, | ||
| RouteMethodParameterInfo mp, | ||
| string typeFqn, | ||
| Location attributeLocation) | ||
| { | ||
| // Look up expected types for this constraint using shared RouteValidator | ||
| if (!RouteValidator.ConstraintToTypes.TryGetValue(constraint, | ||
| out var expectedTypes)) | ||
| { | ||
| return; // Unknown constraint (e.g., custom) - skip validation | ||
| } | ||
|
|
||
| // Get the actual type, unwrapping Nullable<T> for optional parameters | ||
| var actualTypeFqn = typeFqn.UnwrapNullable(rp.IsOptional || mp.IsNullable); | ||
|
|
||
| // Check if actual type matches any expected type | ||
| if (!DoesTypeMatchConstraint(actualTypeFqn, expectedTypes)) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create( | ||
| Descriptors.RouteConstraintTypeMismatch, | ||
| attributeLocation, | ||
| rp.Name, | ||
| constraint, | ||
| expectedTypes[0], | ||
| mp.Name, | ||
| NormalizeTypeName(typeFqn))); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if an actual type matches any of the expected types for a constraint. | ||
| /// </summary> | ||
| private static bool DoesTypeMatchConstraint(string actualTypeFqn, IEnumerable<string> expectedTypes) | ||
| { | ||
| foreach (var expected in expectedTypes) | ||
| { | ||
| if (TypeNamesMatch(actualTypeFqn, expected)) | ||
|
Comment on lines
+107
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== RouteMethodParameterInfo definition =="
rg -n -C3 'RouteMethodParameterInfo' src/ErrorOrX.Generators
echo
echo "== String-based type comparison / nullable unwrapping in generator sources =="
rg -n -C2 'TypeNamesEqual\(|UnwrapNullable\(|NormalizeTypeName\(' src/ErrorOrX.Generators
echo
echo "== ErrorOrContext nullable helpers =="
rg -n -C2 'class ErrorOrContext|UnwrapNullable' src/ErrorOrX.GeneratorsRepository: ANcpLua/ErrorOrX Length of output: 32891 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Call sites of ValidateConstraintTypes in analyzer =="
rg -n -B2 -A2 'ValidateConstraintTypes\(' src/ErrorOrX.Generators/Analyzers/
echo
echo "== ErrorOrEndpointAnalyzer main entry point =="
rg -n 'AnalyzeSymbol|SymbolAnalysisContext' src/ErrorOrX.Generators/Analyzers/ErrorOrEndpointAnalyzer.cs | head -20
echo
echo "== How method symbol is accessed in analyzer =="
rg -n 'IMethodSymbol\|context\.' src/ErrorOrX.Generators/Analyzers/ErrorOrEndpointAnalyzer.RouteValidation.cs | head -15Repository: ANcpLua/ErrorOrX Length of output: 1504 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== AnalyzeMethod signature and how it uses context =="
rg -n -A30 'private static void AnalyzeMethod' src/ErrorOrX.Generators/Analyzers/ErrorOrEndpointAnalyzer.cs
echo
echo "== What does SymbolAnalysisContext expose =="
rg -n 'context\.Symbol\|context\.Compilation' src/ErrorOrX.Generators/Analyzers/ErrorOrEndpointAnalyzer.cs | head -10Repository: ANcpLua/ErrorOrX Length of output: 1492 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Full AnalyzeEndpoint method =="
rg -n -A50 'private static void AnalyzeEndpoint' src/ErrorOrX.Generators/Analyzers/ErrorOrEndpointAnalyzer.cs | head -80
echo
echo "== RouteMethodParameterInfo creation =="
rg -n -B5 -A10 'BuildRouteParameterLookup.*methodParams' src/ErrorOrX.Generators/Analyzers/ErrorOrEndpointAnalyzer.csRepository: ANcpLua/ErrorOrX Length of output: 2518 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== RouteBindingHelper.BindRouteParameters implementation =="
rg -n -A50 'public static.*BindRouteParameters' src/ErrorOrX.Generators/Validation/RouteBindingHelper.cs | head -70
echo
echo "== Where RouteMethodParameterInfo is constructed =="
rg -n -B3 -A3 'new RouteMethodParameterInfo' src/ErrorOrX.Generators/Repository: ANcpLua/ErrorOrX Length of output: 2973 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== EndpointParameter definition =="
rg -n -A15 'record.*EndpointParameter|struct.*EndpointParameter' src/ErrorOrX.Generators/Models/
echo
echo "== ErrorOrEndpointGenerator.BindParameters implementation (first 100 lines) =="
rg -n -A100 'public static.*BindParameters' src/ErrorOrX.Generators/Core/ErrorOrEndpointGenerator.ParameterBinding.cs | head -120Repository: ANcpLua/ErrorOrX Length of output: 2235 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Search for BindParameters method =="
rg -n 'BindParameters' src/ErrorOrX.Generators/Core/ --type cs
echo
echo "== Search for where EndpointParameter is created from IParameterSymbol =="
rg -n -B3 -A8 'IParameterSymbol' src/ErrorOrX.Generators/Core/ErrorOrEndpointGenerator.ParameterBinding.cs | head -100Repository: ANcpLua/ErrorOrX Length of output: 1656 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== RouteValidator.ValidateConstraintTypes (generator phase) =="
rg -n -A30 'public static.*ValidateConstraintTypes' src/ErrorOrX.Generators/Validation/RouteValidator.cs | head -50
echo
echo "== Check if RouteValidator uses symbols or strings =="
rg -n 'MatchesExpectedType\|TypeNamesEqual\|NormalizeTypeName' src/ErrorOrX.Generators/Validation/RouteValidator.csRepository: ANcpLua/ErrorOrX Length of output: 1780 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== How is IMethodSymbol.Parameters used in the generator =="
rg -n 'method\.Parameters' src/ErrorOrX.Generators/Core/ | head -20
echo
echo "== Does RouteValidator have access to method symbol in generator phase? =="
rg -n -B5 'ValidateConstraintTypes.*method' src/ErrorOrX.Generators/Validation/RouteValidator.cs | head -30
echo
echo "== Can we retrieve parameter symbol from method given parameter name? =="
rg -n 'method\.Parameters.*FirstOrDefault\|method\.Parameters.*Where' src/ErrorOrX.Generators/Repository: ANcpLua/ErrorOrX Length of output: 2148 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Analyzer call: does it have access to method.Parameters? =="
rg -n -B10 -A10 'ValidateConstraintTypes.*in context' src/ErrorOrX.Generators/Analyzers/ErrorOrEndpointAnalyzer.cs | grep -A20 'AnalyzeEndpoint'
echo
echo "== Check if method symbol is available when ValidateConstraintTypes is called =="
rg -n -B15 'ValidateConstraintTypes\(in context' src/ErrorOrX.Generators/Analyzers/ErrorOrEndpointAnalyzer.cs
echo
echo "== Full context of where ValidateConstraintTypes is invoked in analyzer =="
sed -n '110,150p' src/ErrorOrX.Generators/Analyzers/ErrorOrEndpointAnalyzer.csRepository: ANcpLua/ErrorOrX Length of output: 123 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Get lines around the ValidateConstraintTypes invocation in analyzer =="
cat -n src/ErrorOrX.Generators/Analyzers/ErrorOrEndpointAnalyzer.cs | sed -n '130,160p'
echo
echo "== Get the full AnalyzeEndpoint method =="
cat -n src/ErrorOrX.Generators/Analyzers/ErrorOrEndpointAnalyzer.cs | sed -n '87,175p'Repository: ANcpLua/ErrorOrX Length of output: 5796 Pass the method symbol to route-constraint validation and use Roslyn type comparison. The analyzer calls The String-based type comparison can drift on generic aliases, nullable lifting semantics, and nested type normalization, leading to inconsistent EOE020 diagnostics. 🤖 Prompt for AI Agents |
||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private static List<string> ValidateRoutePattern(string pattern) | ||
| { | ||
| var issues = new List<string>(); | ||
|
|
||
| if (string.IsNullOrWhiteSpace(pattern)) | ||
| { | ||
| issues.Add("Route pattern cannot be empty"); | ||
| return issues; | ||
| } | ||
|
|
||
| // Strip escaped braces before validation (matches RouteValidator behavior) | ||
| // This prevents false positives for routes like /api/{{version}}/users | ||
| var escapedStripped = pattern.Replace("{{", "").Replace("}}", ""); | ||
|
|
||
| // Check for empty parameter names: {} | ||
| if (escapedStripped.Contains("{}")) | ||
| issues.Add("Route contains empty parameter '{}'. Parameter names are required"); | ||
|
|
||
| // Check for unclosed braces | ||
| var openCount = escapedStripped.Count(static c => c == '{'); | ||
| var closeCount = escapedStripped.Count(static c => c == '}'); | ||
| if (openCount != closeCount) issues.Add($"Route has mismatched braces: {openCount} '{{' and {closeCount} '}}'"); | ||
|
|
||
| // Check for duplicate parameter names using RouteValidator (single source of truth) | ||
| var paramNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
| foreach (var rp in RouteValidator.ExtractRouteParameters(pattern)) | ||
| { | ||
| if (!paramNames.Add(rp.Name)) | ||
| issues.Add($"Route contains duplicate parameter '{{{rp.Name}}}'"); | ||
| } | ||
|
|
||
| return issues; | ||
| } | ||
|
|
||
| private static bool IsStringType(string typeFqn) | ||
| { | ||
| return typeFqn.IsStringType(); | ||
| } | ||
|
|
||
| private static bool TypeNamesMatch(string actualFqn, string expected) | ||
| { | ||
| return actualFqn.TypeNamesEqual(expected); | ||
| } | ||
|
|
||
| private static string NormalizeTypeName(string typeFqn) | ||
| { | ||
| return typeFqn.NormalizeTypeName(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 HIGH RISK
The body source counting logic should account for implicit DTO parameters (complex types not bound elsewhere) and must verify that parameters of type
StreamorIFormFileare not explicitly bound to other sources like[FromServices]or[FromRoute]before counting them as body sources.