Skip to content

Improve code coverage by skipping unreachable code #1705

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

Merged
merged 2 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConsistencyGuard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace JsonApiDotNetCore.OpenApi.Swashbuckle;

#pragma warning disable AV1008 // Class should not be static

internal static class ConsistencyGuard
{
[ExcludeFromCodeCoverage]
public static void ThrowIf([DoesNotReturnIf(true)] bool condition)
{
if (condition)
{
throw new UnreachableException();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Reflection;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
Expand Down Expand Up @@ -115,18 +114,20 @@ private static List<ActionDescriptor> AddJsonApiMetadataToAction(ActionDescripto

private static void UpdateProducesResponseTypeAttribute(ActionDescriptor endpoint, Type responseDocumentType)
{
ProducesResponseTypeAttribute? attribute = null;

if (ProducesJsonApiResponseDocument(endpoint))
{
var producesResponse = endpoint.GetFilterMetadata<ProducesResponseTypeAttribute>();

if (producesResponse != null)
{
producesResponse.Type = responseDocumentType;
return;
attribute = producesResponse;
}
}

throw new UnreachableException();
ConsistencyGuard.ThrowIf(attribute == null);
attribute.Type = responseDocumentType;
}

private static bool ProducesJsonApiResponseDocument(ActionDescriptor endpoint)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Reflection;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
Expand Down Expand Up @@ -43,11 +42,7 @@ public JsonApiEndpointMetadataContainer Get(MethodInfo controllerAction)
}

ResourceType? primaryResourceType = _controllerResourceMapping.GetResourceTypeForController(controllerAction.ReflectedType);

if (primaryResourceType == null)
{
throw new UnreachableException();
}
ConsistencyGuard.ThrowIf(primaryResourceType == null);

IJsonApiRequestMetadata? requestMetadata = GetRequestMetadata(endpoint, primaryResourceType);
IJsonApiResponseMetadata? responseMetadata = GetResponseMetadata(endpoint, primaryResourceType);
Expand Down
154 changes: 106 additions & 48 deletions src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Net;
using System.Reflection;
using JsonApiDotNetCore.Configuration;
Expand Down Expand Up @@ -97,20 +96,56 @@ private static bool IsEndpointAvailable(JsonApiEndpoints endpoint, ResourceType

// For an overridden JSON:API action method in a partial class to show up, it's flag must be turned on in [Resource].
// Otherwise, it is considered to be an action method that throws because the endpoint is unavailable.
return endpoint switch
{
JsonApiEndpoints.GetCollection => availableEndpoints.HasFlag(JsonApiEndpoints.GetCollection),
JsonApiEndpoints.GetSingle => availableEndpoints.HasFlag(JsonApiEndpoints.GetSingle),
JsonApiEndpoints.GetSecondary => availableEndpoints.HasFlag(JsonApiEndpoints.GetSecondary),
JsonApiEndpoints.GetRelationship => availableEndpoints.HasFlag(JsonApiEndpoints.GetRelationship),
JsonApiEndpoints.Post => availableEndpoints.HasFlag(JsonApiEndpoints.Post),
JsonApiEndpoints.PostRelationship => availableEndpoints.HasFlag(JsonApiEndpoints.PostRelationship),
JsonApiEndpoints.Patch => availableEndpoints.HasFlag(JsonApiEndpoints.Patch),
JsonApiEndpoints.PatchRelationship => availableEndpoints.HasFlag(JsonApiEndpoints.PatchRelationship),
JsonApiEndpoints.Delete => availableEndpoints.HasFlag(JsonApiEndpoints.Delete),
JsonApiEndpoints.DeleteRelationship => availableEndpoints.HasFlag(JsonApiEndpoints.DeleteRelationship),
_ => throw new UnreachableException()
};
return IncludesEndpoint(endpoint, availableEndpoints);
}

private static bool IncludesEndpoint(JsonApiEndpoints endpoint, JsonApiEndpoints availableEndpoints)
{
bool? isIncluded = null;

if (endpoint == JsonApiEndpoints.GetCollection)
{
isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.GetCollection);
}
else if (endpoint == JsonApiEndpoints.GetSingle)
{
isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.GetSingle);
}
else if (endpoint == JsonApiEndpoints.GetSecondary)
{
isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.GetSecondary);
}
else if (endpoint == JsonApiEndpoints.GetRelationship)
{
isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.GetRelationship);
}
else if (endpoint == JsonApiEndpoints.Post)
{
isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.Post);
}
else if (endpoint == JsonApiEndpoints.PostRelationship)
{
isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.PostRelationship);
}
else if (endpoint == JsonApiEndpoints.Patch)
{
isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.Patch);
}
else if (endpoint == JsonApiEndpoints.PatchRelationship)
{
isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.PatchRelationship);
}
else if (endpoint == JsonApiEndpoints.Delete)
{
isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.Delete);
}
else if (endpoint == JsonApiEndpoints.DeleteRelationship)
{
isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.DeleteRelationship);
}

ConsistencyGuard.ThrowIf(isIncluded == null);
return isIncluded.Value;
}

private static JsonApiEndpoints GetGeneratedControllerEndpoints(ResourceType resourceType)
Expand Down Expand Up @@ -158,29 +193,40 @@ private static HttpStatusCode[] GetSuccessStatusCodesForEndpoint(JsonApiEndpoint
];
}

return endpoint.Value switch
HttpStatusCode[]? statusCodes = null;

if (endpoint.Value is JsonApiEndpoints.GetCollection or JsonApiEndpoints.GetSingle or JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship)
{
JsonApiEndpoints.GetCollection or JsonApiEndpoints.GetSingle or JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship =>
statusCodes =
[
HttpStatusCode.OK,
HttpStatusCode.NotModified
],
JsonApiEndpoints.Post =>
];
}
else if (endpoint.Value == JsonApiEndpoints.Post)
{
statusCodes =
[
HttpStatusCode.Created,
HttpStatusCode.NoContent
],
JsonApiEndpoints.Patch =>
];
}
else if (endpoint.Value == JsonApiEndpoints.Patch)
{
statusCodes =
[
HttpStatusCode.OK,
HttpStatusCode.NoContent
],
JsonApiEndpoints.Delete or JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship =>
[
HttpStatusCode.NoContent
],
_ => throw new UnreachableException()
};
];
}
else if (endpoint.Value is JsonApiEndpoints.Delete or JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or
JsonApiEndpoints.DeleteRelationship)
{
statusCodes = [HttpStatusCode.NoContent];
}

ConsistencyGuard.ThrowIf(statusCodes == null);
return statusCodes;
}

private HttpStatusCode[] GetErrorStatusCodesForEndpoint(JsonApiEndpointWrapper endpoint, ResourceType? resourceType)
Expand All @@ -200,46 +246,58 @@ private HttpStatusCode[] GetErrorStatusCodesForEndpoint(JsonApiEndpointWrapper e
// Condition doesn't apply to atomic operations, because Forbidden is also used when an operation is not accessible.
ClientIdGenerationMode clientIdGeneration = resourceType?.ClientIdGeneration ?? _options.ClientIdGeneration;

return endpoint.Value switch
HttpStatusCode[]? statusCodes = null;

if (endpoint.Value == JsonApiEndpoints.GetCollection)
{
statusCodes = [HttpStatusCode.BadRequest];
}
else if (endpoint.Value is JsonApiEndpoints.GetSingle or JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship)
{
JsonApiEndpoints.GetCollection => [HttpStatusCode.BadRequest],
JsonApiEndpoints.GetSingle or JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship =>
statusCodes =
[
HttpStatusCode.BadRequest,
HttpStatusCode.NotFound
],
JsonApiEndpoints.Post when clientIdGeneration == ClientIdGenerationMode.Forbidden =>
];
}
else if (endpoint.Value == JsonApiEndpoints.Post && clientIdGeneration == ClientIdGenerationMode.Forbidden)
{
statusCodes =
[
HttpStatusCode.BadRequest,
HttpStatusCode.Forbidden,
HttpStatusCode.NotFound,
HttpStatusCode.Conflict,
HttpStatusCode.UnprocessableEntity
],
JsonApiEndpoints.Post =>
[
HttpStatusCode.BadRequest,
HttpStatusCode.NotFound,
HttpStatusCode.Conflict,
HttpStatusCode.UnprocessableEntity
],
JsonApiEndpoints.Patch =>
];
}
else if (endpoint.Value is JsonApiEndpoints.Post or JsonApiEndpoints.Patch)
{
statusCodes =
[
HttpStatusCode.BadRequest,
HttpStatusCode.NotFound,
HttpStatusCode.Conflict,
HttpStatusCode.UnprocessableEntity
],
JsonApiEndpoints.Delete => [HttpStatusCode.NotFound],
JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship =>
];
}
else if (endpoint.Value == JsonApiEndpoints.Delete)
{
statusCodes = [HttpStatusCode.NotFound];
}
else if (endpoint.Value is JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship)
{
statusCodes =
[
HttpStatusCode.BadRequest,
HttpStatusCode.NotFound,
HttpStatusCode.Conflict,
HttpStatusCode.UnprocessableEntity
],
_ => throw new UnreachableException()
};
];
}

ConsistencyGuard.ThrowIf(statusCodes == null);
return statusCodes;
}

private void SetRequestMetadata(ActionModel action, JsonApiEndpointWrapper endpoint)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using Humanizer;
Expand Down Expand Up @@ -64,23 +63,14 @@ public string GetOpenApiOperationId(ApiDescription endpoint)
private static string GetTemplate(ApiDescription endpoint)
{
Type bodyType = GetBodyType(endpoint);

if (!SchemaOpenTypeToOpenApiOperationIdTemplateMap.TryGetValue(bodyType, out string? template))
{
throw new UnreachableException();
}

ConsistencyGuard.ThrowIf(!SchemaOpenTypeToOpenApiOperationIdTemplateMap.TryGetValue(bodyType, out string? template));
return template;
}

private static Type GetBodyType(ApiDescription endpoint)
{
var producesResponseTypeAttribute = endpoint.ActionDescriptor.GetFilterMetadata<ProducesResponseTypeAttribute>();

if (producesResponseTypeAttribute == null)
{
throw new UnreachableException();
}
ConsistencyGuard.ThrowIf(producesResponseTypeAttribute == null);

ControllerParameterDescriptor? requestBodyDescriptor = endpoint.ActionDescriptor.GetBodyParameterDescriptor();
Type bodyType = (requestBodyDescriptor?.ParameterType ?? producesResponseTypeAttribute.Type).ConstructedToOpenType();
Expand All @@ -95,10 +85,8 @@ private static Type GetBodyType(ApiDescription endpoint)

private string ApplyTemplate(string openApiOperationIdTemplate, ResourceType? resourceType, ApiDescription endpoint)
{
if (endpoint.RelativePath == null || endpoint.HttpMethod == null)
{
throw new UnreachableException();
}
ConsistencyGuard.ThrowIf(endpoint.RelativePath == null);
ConsistencyGuard.ThrowIf(endpoint.HttpMethod == null);

string method = endpoint.HttpMethod.ToLowerInvariant();
string relationshipName = openApiOperationIdTemplate.Contains("[RelationshipName]") ? endpoint.RelativePath.Split('/').Last() : string.Empty;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics;
using Microsoft.OpenApi.Models;

namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
Expand All @@ -20,10 +19,7 @@ public static void ReorderProperties(this OpenApiSchema fullSchema, IEnumerable<
}
}

if (fullSchema.Properties.Count != propertiesInOrder.Count)
{
throw new UnreachableException();
}
ConsistencyGuard.ThrowIf(fullSchema.Properties.Count != propertiesInOrder.Count);

fullSchema.Properties = propertiesInOrder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Reflection;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.ResourceObjects;
Expand Down Expand Up @@ -64,11 +63,7 @@ public OpenApiSchema GenerateSchema(Type dataContainerSchemaType, ResourceType r
private static Type GetElementTypeOfDataProperty(Type dataContainerConstructedType, ResourceType resourceType)
{
PropertyInfo? dataProperty = dataContainerConstructedType.GetProperty("Data");

if (dataProperty == null)
{
throw new UnreachableException();
}
ConsistencyGuard.ThrowIf(dataProperty == null);

Type innerPropertyType = dataProperty.PropertyType.ConstructedToOpenType().IsAssignableTo(typeof(ICollection<>))
? dataProperty.PropertyType.GenericTypeArguments[0]
Expand All @@ -79,10 +74,7 @@ private static Type GetElementTypeOfDataProperty(Type dataContainerConstructedTy
return typeof(DataInResponse<>).MakeGenericType(resourceType.ClrType);
}

if (!innerPropertyType.IsGenericType)
{
throw new UnreachableException();
}
ConsistencyGuard.ThrowIf(!innerPropertyType.IsGenericType);

return innerPropertyType;
}
Expand Down
Loading
Loading