Skip to content

Commit

Permalink
[IDP-1588] Support Named arguments in Annotation for Type mismatch ru…
Browse files Browse the repository at this point in the history
…le (#19)

* [IDP-1588] Support Named arguments in Annotation for Type mismatch rule

* Fix formatting

* Address CR comments

* address CR comments

* add parenthesis
  • Loading branch information
heqianwang authored Jun 4, 2024
1 parent fd4ad0f commit e7a3c12
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -136,48 +136,105 @@ public void ValidateEndpointResponseWithAnnotationType(SymbolAnalysisContext con
private void ValidateAnnotationWithTypedResult(SymbolAnalysisContext context, AttributeData attribute,
Dictionary<int, List<ITypeSymbol>> methodSignatureStatusCodeToTypeMap)
{
if (attribute.AttributeClass == null || attribute.ConstructorArguments.Length == 0)
if (attribute.AttributeClass == null || (attribute.ConstructorArguments.Length == 0 && attribute.NamedArguments.Length == 0))
{
return;
}

// For the annotations [ProducesResponseType(<StatusCode>)] and [ProducesResponseType(<typeof()>, <StatusCode>)]
if (attribute.AttributeClass.Equals(this.ProducesResponseSymbol, SymbolEqualityComparer.Default))
// For the case when the Type is a named argument
if (TryGetTypeFromNamedArguments(attribute, out var typeFromNamedArgument) && typeFromNamedArgument != null)
{
if (attribute.ConstructorArguments.Length == 1)
this.HandleTypeAsNamedArgument(context, attribute, methodSignatureStatusCodeToTypeMap, typeFromNamedArgument);
}

// For the annotations [ProducesResponseType(<StatusCode>)], [ProducesResponseType(<typeof()>, <StatusCode>)] and [ProducesResponseType(<StatusCode>, Type = <typeof()>)]
else if (attribute.AttributeClass.Equals(this.ProducesResponseSymbol, SymbolEqualityComparer.Default))
{
this.HandleProducesResponseAnnotation(context, attribute, methodSignatureStatusCodeToTypeMap);
}
// For the annotations [ProducesResponseType<T>(<StatusCode>)]
else if (attribute.AttributeClass.ConstructedFrom.Equals(this.ProducesResponseOfTSymbol, SymbolEqualityComparer.Default))
{
this.HandleProducesResponseOfTAnnotation(context, attribute, methodSignatureStatusCodeToTypeMap);
}

// For the annotations [SwaggerResponse(<StatusCode>, "description", <typeof()>] and [SwaggerResponse(<StatusCode>, "description", Type = <typeof()>]
else if (attribute.AttributeClass.ConstructedFrom.Equals(this.SwaggerResponseSymbol, SymbolEqualityComparer.Default))
{
this.HandleSwaggerResponseAnnotation(context, attribute, methodSignatureStatusCodeToTypeMap);
}
}

private void HandleTypeAsNamedArgument(SymbolAnalysisContext context, AttributeData attribute,
Dictionary<int, List<ITypeSymbol>> methodSignatureStatusCodeToTypeMap, ITypeSymbol typeFromNamedArgument)
{
if (attribute.ConstructorArguments[0].Value is int statusCodeValue)
{
ValidateAnnotationForTypeMismatch(attribute, statusCodeValue, typeFromNamedArgument, methodSignatureStatusCodeToTypeMap, context);
}
}

private void HandleProducesResponseAnnotation(SymbolAnalysisContext context, AttributeData attribute,
Dictionary<int, List<ITypeSymbol>> methodSignatureStatusCodeToTypeMap)
{
// If there is a single argument, then StatusCode is the first argument.
if (attribute.ConstructorArguments.Length == 1)
{
if (attribute.ConstructorArguments[0].Value is not int statusCodeValue)
{
if (attribute.ConstructorArguments[0].Value is int statusCodeValue)
{
if (this._statusCodeToResultsMap.TryGetValue(statusCodeValue, out var type))
{
ValidateAnnotationForTypeMismatch(attribute, statusCodeValue, type, methodSignatureStatusCodeToTypeMap, context);
}
}
return;
}
else if (attribute.ConstructorArguments[1].Value is int statusCodeValue && attribute.ConstructorArguments[0].Value is ITypeSymbol type)

if (this._statusCodeToResultsMap.TryGetValue(statusCodeValue, out var type))
{
ValidateAnnotationForTypeMismatch(attribute, statusCodeValue, type, methodSignatureStatusCodeToTypeMap, context);
}
}
// For the annotations [ProducesResponseType<T>(<StatusCode>)]
else if (attribute.AttributeClass.ConstructedFrom.Equals(this.ProducesResponseOfTSymbol, SymbolEqualityComparer.Default))
// If there are two arguments, then StatusCode is the second argument.
else if (attribute.ConstructorArguments[1].Value is int statusCodeValue && attribute.ConstructorArguments[0].Value is ITypeSymbol type)
{
if (attribute.ConstructorArguments[0].Value is int statusCodeValue)
{
ValidateAnnotationForTypeMismatch(attribute, statusCodeValue, attribute.AttributeClass.TypeArguments[0], methodSignatureStatusCodeToTypeMap, context);
}
ValidateAnnotationForTypeMismatch(attribute, statusCodeValue, type, methodSignatureStatusCodeToTypeMap, context);
}
}

// For the annotations [SwaggerResponse(<StatusCode>, "description", <typeof()>]
else if (attribute.AttributeClass.ConstructedFrom.Equals(this.SwaggerResponseSymbol, SymbolEqualityComparer.Default))
private void HandleProducesResponseOfTAnnotation(SymbolAnalysisContext context, AttributeData attribute,
Dictionary<int, List<ITypeSymbol>> methodSignatureStatusCodeToTypeMap)
{
if (attribute.ConstructorArguments[0].Value is int statusCodeValue && attribute.AttributeClass != null)
{
ValidateAnnotationForTypeMismatch(attribute, statusCodeValue, attribute.AttributeClass.TypeArguments[0], methodSignatureStatusCodeToTypeMap, context);
}
}

private void HandleSwaggerResponseAnnotation(SymbolAnalysisContext context, AttributeData attribute,
Dictionary<int, List<ITypeSymbol>> methodSignatureStatusCodeToTypeMap)
{
if (attribute.ConstructorArguments.Length > 2 && attribute.ConstructorArguments[0].Value is int statusCodeValue)
{
if (attribute.ConstructorArguments.Length > 2 && attribute.ConstructorArguments[0].Value is int statusCodeValue && attribute.ConstructorArguments[2].Value is ITypeSymbol type)
if (attribute.ConstructorArguments[2].Value is ITypeSymbol typeFromAnnotation)
{
ValidateAnnotationForTypeMismatch(attribute, statusCodeValue, typeFromAnnotation, methodSignatureStatusCodeToTypeMap, context);
}
else if (this._statusCodeToResultsMap.TryGetValue(statusCodeValue, out var type))
{
ValidateAnnotationForTypeMismatch(attribute, statusCodeValue, type, methodSignatureStatusCodeToTypeMap, context);
}
}
}

private static bool TryGetTypeFromNamedArguments(AttributeData attribute, out ITypeSymbol? type)
{
var typeNamedArgument = attribute.NamedArguments.FirstOrDefault(kp => kp.Key == "Type");
if (typeNamedArgument.Value.Value is ITypeSymbol matchedType)
{
type = matchedType;
return true;
}

type = null;
return false;
}

// Result<Ok<type>, Notfound>
private Dictionary<int, List<ITypeSymbol>> GetMethodReturnStatusCodeToType(ITypeSymbol methodSymbol)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ protected override CompilationOptions CreateCompilationOptions()
// Specify the language version and that diagnostics should be reported for missing documentation.
protected override ParseOptions CreateParseOptions()
{
return new CSharpParseOptions(LanguageVersion.CSharp11, DocumentationMode.Diagnose);
return new CSharpParseOptions(LanguageVersion.CSharp12, DocumentationMode.Diagnose);
}

protected BaseAnalyzerTest<TAnalyzer> WithSourceCode(string sourceCode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,80 @@ public class AnalyzersController : ControllerBase
await this.WithSourceCode(source)
.RunAsync();
}

[Fact]
public async Task Given_ProducesResponseNamedArgumentsAndCorrectTypedResults_When_Analyze_Then_No_Diagnostic()
{
const string source = """
public class AnalyzersController : ControllerBase
{
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))]
[ProducesResponseType(statusCode: StatusCodes.Status202Accepted, type: typeof(string))]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<Results<Ok<string>, Accepted<string>, Forbidden>> GetSampleEndpoint() => throw null;
}
""";

await this.WithSourceCode(source)
.RunAsync();
}

[Fact]
public async Task Given_ProducesResponseNamedArgumentsAndMismatchTypedResults_When_Analyze_Then_Diagnostic()
{
const string source = """
public class AnalyzersController : ControllerBase
{
[HttpGet]
[{|WLOAS001:ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))|}]
[{|WLOAS001:ProducesResponseType(statusCode: StatusCodes.Status202Accepted, type: typeof(string))|}]
[{|WLOAS001:ProducesResponseType(StatusCodes.Status403Forbidden)|}]
public async Task<Results<Ok<int>, Accepted, Forbidden<string>>> GetSampleEndpoint() => throw null;
}
""";

await this.WithSourceCode(source)
.RunAsync();
}

[Fact]
public async Task Given_SwaggerResponseNamedArgumentsAndCorrectTypedResults_When_Analyze_Then_No_Diagnostic()
{
const string source = """
public class AnalyzersController : ControllerBase
{
[HttpGet]
[SwaggerResponse(StatusCodes.Status200OK, Type = typeof(string))]
[SwaggerResponse(statusCode: StatusCodes.Status202Accepted, "Return string", Type = typeof(string))]
[SwaggerResponse(type: typeof(string), statusCode: StatusCodes.Status400BadRequest)]
[SwaggerResponse(StatusCodes.Status403Forbidden, "Returns string", ContentTypes = ["application/json"])]
[SwaggerResponse(StatusCodes.Status500InternalServerError, "Returns string", typeof(string), ContentTypes = ["application/json"])]
public async Task<Results<Ok<string>, Accepted<string>, BadRequest<string>, Forbidden, InternalServerError<string>>> GetSampleEndpoint() => throw null;
}
""";

await this.WithSourceCode(source)
.RunAsync();
}

[Fact]
public async Task Given_SwaggerResponseNamedArgumentsAndMismatchTypedResults_When_Analyze_Then_No_Diagnostic()
{
const string source = """
public class AnalyzersController : ControllerBase
{
[HttpGet]
[{|WLOAS001:SwaggerResponse(StatusCodes.Status200OK, Type = typeof(string))|}]
[{|WLOAS001:SwaggerResponse(statusCode: StatusCodes.Status202Accepted, "Return string", Type = typeof(string))|}]
[{|WLOAS001:SwaggerResponse(type: typeof(string), statusCode: StatusCodes.Status400BadRequest)|}]
[{|WLOAS001:SwaggerResponse(StatusCodes.Status403Forbidden, "Returns string", ContentTypes = ["application/json"])|}]
[{|WLOAS001:SwaggerResponse(StatusCodes.Status500InternalServerError, "Returns string", typeof(string), ContentTypes = ["application/json"])|}]
public async Task<Results<Ok<int>, Accepted, BadRequest<int>, Forbidden<int>, InternalServerError>> GetSampleEndpoint() => throw null;
}
""";

await this.WithSourceCode(source)
.RunAsync();
}
}

0 comments on commit e7a3c12

Please sign in to comment.