Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 8311fd8

Browse files
authoredSep 26, 2018
Include the response type in ProducesResponseType for client errors (#8490)
* Include the response type in ProducesResponseType for client errors * Refactor ActualApiResponseMetadata discovery in to a separate more manageable type * Annotate action result ctors and helper methods that specify the "object" value with attribute * Modify the discovery of parameters to match ActionResultObjectValueAttribute and ActionResultStatusCodeAttribute by name to allow users to write and annotate custom helper methods and action results, a la NotNullAttribute. Fixes #8345
1 parent 5b8b3a0 commit 8311fd8

File tree

43 files changed

+1235
-535
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1235
-535
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using Microsoft.CodeAnalysis;
45
using Microsoft.CodeAnalysis.CSharp.Syntax;
56

67
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
@@ -9,22 +10,26 @@ internal readonly struct ActualApiResponseMetadata
910
{
1011
private readonly int? _statusCode;
1112

12-
public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement)
13+
public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement, ITypeSymbol returnType)
1314
{
1415
ReturnStatement = returnStatement;
16+
ReturnType = returnType;
1517
_statusCode = null;
1618
}
1719

18-
public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement, int statusCode)
20+
public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement, int statusCode, ITypeSymbol returnType)
1921
{
2022
ReturnStatement = returnStatement;
2123
_statusCode = statusCode;
24+
ReturnType = returnType;
2225
}
2326

2427
public ReturnStatementSyntax ReturnStatement { get; }
2528

2629
public int StatusCode => _statusCode.Value;
2730

2831
public bool IsDefaultResponse => _statusCode == null;
32+
33+
public ITypeSymbol ReturnType { get; }
2934
}
3035
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Threading;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CSharp;
10+
using Microsoft.CodeAnalysis.CSharp.Syntax;
11+
12+
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
13+
{
14+
public static class ActualApiResponseMetadataFactory
15+
{
16+
private static readonly Func<SyntaxNode, bool> _shouldDescendIntoChildren = ShouldDescendIntoChildren;
17+
18+
/// <summary>
19+
/// This method looks at individual return statments and attempts to parse the status code and the return type.
20+
/// Given a <see cref="MethodDeclarationSyntax"/> for an action, this method inspects return statements in the body.
21+
/// If the returned type is not assignable from IActionResult, it assumes that an "object" value is being returned. e.g. return new Person();
22+
/// For return statements returning an action result, it attempts to infer the status code and return type. Helper methods in controller,
23+
/// values set in initializer and new-ing up an IActionResult instance are supported.
24+
/// </summary>
25+
internal static bool TryGetActualResponseMetadata(
26+
in ApiControllerSymbolCache symbolCache,
27+
SemanticModel semanticModel,
28+
MethodDeclarationSyntax methodSyntax,
29+
CancellationToken cancellationToken,
30+
out IList<ActualApiResponseMetadata> actualResponseMetadata)
31+
{
32+
actualResponseMetadata = new List<ActualApiResponseMetadata>();
33+
34+
var allReturnStatementsReadable = true;
35+
36+
foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType<ReturnStatementSyntax>())
37+
{
38+
if (returnStatementSyntax.IsMissing || returnStatementSyntax.Expression == null || returnStatementSyntax.Expression.IsMissing)
39+
{
40+
// Ignore malformed return statements.
41+
allReturnStatementsReadable = false;
42+
continue;
43+
}
44+
45+
var responseMetadata = InspectReturnStatementSyntax(
46+
symbolCache,
47+
semanticModel,
48+
returnStatementSyntax,
49+
cancellationToken);
50+
51+
if (responseMetadata != null)
52+
{
53+
actualResponseMetadata.Add(responseMetadata.Value);
54+
}
55+
else
56+
{
57+
allReturnStatementsReadable = false;
58+
}
59+
}
60+
61+
return allReturnStatementsReadable;
62+
}
63+
64+
internal static ActualApiResponseMetadata? InspectReturnStatementSyntax(
65+
in ApiControllerSymbolCache symbolCache,
66+
SemanticModel semanticModel,
67+
ReturnStatementSyntax returnStatementSyntax,
68+
CancellationToken cancellationToken)
69+
{
70+
var returnExpression = returnStatementSyntax.Expression;
71+
var typeInfo = semanticModel.GetTypeInfo(returnExpression, cancellationToken);
72+
if (typeInfo.Type == null || typeInfo.Type.TypeKind == TypeKind.Error)
73+
{
74+
return null;
75+
}
76+
77+
var statementReturnType = typeInfo.Type;
78+
79+
if (!symbolCache.IActionResult.IsAssignableFrom(statementReturnType))
80+
{
81+
// Return expression is not an instance of IActionResult. Must be returning the "model".
82+
return new ActualApiResponseMetadata(returnStatementSyntax, statementReturnType);
83+
}
84+
85+
var defaultStatusCodeAttribute = statementReturnType
86+
.GetAttributes(symbolCache.DefaultStatusCodeAttribute, inherit: true)
87+
.FirstOrDefault();
88+
89+
var statusCode = GetDefaultStatusCode(defaultStatusCodeAttribute);
90+
ITypeSymbol returnType = null;
91+
switch (returnExpression)
92+
{
93+
case InvocationExpressionSyntax invocation:
94+
{
95+
// Covers the 'return StatusCode(200)' case.
96+
var result = InspectMethodArguments(symbolCache, semanticModel, invocation.Expression, invocation.ArgumentList, cancellationToken);
97+
statusCode = result.statusCode ?? statusCode;
98+
returnType = result.returnType;
99+
break;
100+
}
101+
102+
case ObjectCreationExpressionSyntax creation:
103+
{
104+
// Read values from 'return new StatusCodeResult(200) case.
105+
var result = InspectMethodArguments(symbolCache, semanticModel, creation, creation.ArgumentList, cancellationToken);
106+
statusCode = result.statusCode ?? statusCode;
107+
returnType = result.returnType;
108+
109+
// Read values from property assignments e.g. 'return new ObjectResult(...) { StatusCode = 200 }'.
110+
// Property assignments override constructor assigned values and defaults.
111+
result = InspectInitializers(symbolCache, semanticModel, creation.Initializer, cancellationToken);
112+
statusCode = result.statusCode ?? statusCode;
113+
returnType = result.returnType ?? returnType;
114+
break;
115+
}
116+
}
117+
118+
if (statusCode == null)
119+
{
120+
return null;
121+
}
122+
123+
return new ActualApiResponseMetadata(returnStatementSyntax, statusCode.Value, returnType);
124+
}
125+
126+
private static (int? statusCode, ITypeSymbol returnType) InspectInitializers(
127+
in ApiControllerSymbolCache symbolCache,
128+
SemanticModel semanticModel,
129+
InitializerExpressionSyntax initializer,
130+
CancellationToken cancellationToken)
131+
{
132+
int? statusCode = null;
133+
ITypeSymbol typeSymbol = null;
134+
135+
for (var i = 0; initializer != null && i < initializer.Expressions.Count; i++)
136+
{
137+
var expression = initializer.Expressions[i];
138+
139+
if (!(expression is AssignmentExpressionSyntax assignment) ||
140+
!(assignment.Left is IdentifierNameSyntax identifier))
141+
{
142+
continue;
143+
}
144+
145+
var symbolInfo = semanticModel.GetSymbolInfo(identifier, cancellationToken);
146+
if (symbolInfo.Symbol is IPropertySymbol property)
147+
{
148+
if (IsInterfaceImplementation(property, symbolCache.StatusCodeActionResultStatusProperty) &&
149+
TryGetExpressionStatusCode(semanticModel, assignment.Right, cancellationToken, out var statusCodeValue))
150+
{
151+
// Look for assignments to IStatusCodeActionResult.StatusCode
152+
statusCode = statusCodeValue;
153+
}
154+
else if (HasAttributeNamed(property, ApiSymbolNames.ActionResultObjectValueAttribute))
155+
{
156+
// Look for assignment to a property annotated with [ActionResultObjectValue]
157+
typeSymbol = GetExpressionObjectType(semanticModel, assignment.Right, cancellationToken);
158+
}
159+
}
160+
}
161+
162+
return (statusCode, typeSymbol);
163+
}
164+
165+
private static (int? statusCode, ITypeSymbol returnType) InspectMethodArguments(
166+
in ApiControllerSymbolCache symbolCache,
167+
SemanticModel semanticModel,
168+
ExpressionSyntax expression,
169+
BaseArgumentListSyntax argumentList,
170+
CancellationToken cancellationToken)
171+
{
172+
int? statusCode = null;
173+
ITypeSymbol typeSymbol = null;
174+
175+
var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken);
176+
177+
if (symbolInfo.Symbol is IMethodSymbol method)
178+
{
179+
for (var i = 0; i < method.Parameters.Length; i++)
180+
{
181+
var parameter = method.Parameters[i];
182+
if (HasAttributeNamed(parameter, ApiSymbolNames.ActionResultStatusCodeAttribute))
183+
{
184+
var argument = argumentList.Arguments[parameter.Ordinal];
185+
if (TryGetExpressionStatusCode(semanticModel, argument.Expression, cancellationToken, out var statusCodeValue))
186+
{
187+
statusCode = statusCodeValue;
188+
}
189+
}
190+
191+
if (HasAttributeNamed(parameter, ApiSymbolNames.ActionResultObjectValueAttribute))
192+
{
193+
var argument = argumentList.Arguments[parameter.Ordinal];
194+
typeSymbol = GetExpressionObjectType(semanticModel, argument.Expression, cancellationToken);
195+
}
196+
}
197+
}
198+
199+
return (statusCode, typeSymbol);
200+
}
201+
202+
private static ITypeSymbol GetExpressionObjectType(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken)
203+
{
204+
var typeInfo = semanticModel.GetTypeInfo(expression, cancellationToken);
205+
return typeInfo.Type;
206+
}
207+
208+
private static bool TryGetExpressionStatusCode(
209+
SemanticModel semanticModel,
210+
ExpressionSyntax expression,
211+
CancellationToken cancellationToken,
212+
out int statusCode)
213+
{
214+
if (expression is LiteralExpressionSyntax literal && literal.Token.Value is int literalStatusCode)
215+
{
216+
// Covers the 'return StatusCode(200)' case.
217+
statusCode = literalStatusCode;
218+
return true;
219+
}
220+
221+
if (expression is IdentifierNameSyntax || expression is MemberAccessExpressionSyntax)
222+
{
223+
var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken);
224+
225+
if (symbolInfo.Symbol is IFieldSymbol field && field.HasConstantValue && field.ConstantValue is int constantStatusCode)
226+
{
227+
// Covers the 'return StatusCode(StatusCodes.Status200OK)' case.
228+
// It also covers the 'return StatusCode(StatusCode)' case, where 'StatusCode' is a constant field.
229+
statusCode = constantStatusCode;
230+
return true;
231+
}
232+
233+
if (symbolInfo.Symbol is ILocalSymbol local && local.HasConstantValue && local.ConstantValue is int localStatusCode)
234+
{
235+
// Covers the 'return StatusCode(statusCode)' case, where 'statusCode' is a local constant.
236+
statusCode = localStatusCode;
237+
return true;
238+
}
239+
}
240+
241+
statusCode = default;
242+
return false;
243+
}
244+
245+
private static bool ShouldDescendIntoChildren(SyntaxNode syntaxNode)
246+
{
247+
return !syntaxNode.IsKind(SyntaxKind.LocalFunctionStatement) &&
248+
!syntaxNode.IsKind(SyntaxKind.ParenthesizedLambdaExpression) &&
249+
!syntaxNode.IsKind(SyntaxKind.SimpleLambdaExpression) &&
250+
!syntaxNode.IsKind(SyntaxKind.AnonymousMethodExpression);
251+
}
252+
253+
internal static int? GetDefaultStatusCode(AttributeData attribute)
254+
{
255+
if (attribute != null &&
256+
attribute.ConstructorArguments.Length == 1 &&
257+
attribute.ConstructorArguments[0].Kind == TypedConstantKind.Primitive &&
258+
attribute.ConstructorArguments[0].Value is int statusCode)
259+
{
260+
return statusCode;
261+
}
262+
263+
return null;
264+
}
265+
266+
private static bool IsInterfaceImplementation(IPropertySymbol property, IPropertySymbol statusCodeActionResultStatusProperty)
267+
{
268+
if (property.Name != statusCodeActionResultStatusProperty.Name)
269+
{
270+
return false;
271+
}
272+
273+
for (var i = 0; i < property.ExplicitInterfaceImplementations.Length; i++)
274+
{
275+
if (property.ExplicitInterfaceImplementations[i] == statusCodeActionResultStatusProperty)
276+
{
277+
return true;
278+
}
279+
}
280+
281+
var implementedProperty = property.ContainingType.FindImplementationForInterfaceMember(statusCodeActionResultStatusProperty);
282+
return implementedProperty == property;
283+
}
284+
285+
private static bool HasAttributeNamed(ISymbol symbol, string attributeName)
286+
{
287+
var attributes = symbol.GetAttributes();
288+
var length = attributes.Length;
289+
for (var i = 0; i < length; i++)
290+
{
291+
if (attributes[i].AttributeClass.Name == attributeName)
292+
{
293+
return true;
294+
}
295+
}
296+
297+
return false;
298+
}
299+
}
300+
}

0 commit comments

Comments
 (0)
This repository has been archived.