-
Notifications
You must be signed in to change notification settings - Fork 10.2k
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
Update RDG to use interceptors feature #48817
Conversation
2860bdc
to
d1412b1
Compare
c06f2e0
to
2adb5f6
Compare
2adb5f6
to
d45c8f7
Compare
@@ -8,6 +8,7 @@ | |||
<Nullable>Enable</Nullable> | |||
<RootNamespace>Microsoft.AspNetCore.Analyzers</RootNamespace> | |||
<SuppressNullableAttributesImport>true</SuppressNullableAttributesImport> | |||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> |
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.
This change, and stylistic modifications in the analyzers, are a result of new diagnostics emitted from the updated compiler version.
using Microsoft.CodeAnalysis.Completion; | ||
|
||
namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; | ||
|
||
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/49126")] |
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.
A downstream change in Roslyn makes these tests unreliable. A PR with a fix is out but I am quarantining for now so that we can move forward with this change.
return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate); | ||
} | ||
|
||
private static T Cast<T>(Delegate d, T _) where T : Delegate |
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.
Now that we are no longer using strongly-typed delegate types in the overload we need to get a concrete type from the Delegate
parameter so that we can use it in the generated code. This Cast
method allows us to capture the concrete type and to capture default parameters passed to the lambda/method group (see here).
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.
I think it would be good to add a comment here as well to fully explain this since it's kind of obscure (just to help future maintainers).
{ | ||
{{GeneratedCodeAttribute}} | ||
internal sealed class SourceKey | ||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] | ||
file sealed class InterceptsLocationAttribute : Attribute |
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.
No public API is included as part of the interceptors feature in preview so we have to define this type ourselves. We use AllowMultiple = true
here so we can use the same interceptor for Map
action calls that implement the same signature.
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.
This DiagnosticSuppressor
is the recommended workaround for dotnet/roslyn#68669. It specifically suppresses the false-positive diagnostics that are emitted by the linker analyzer when the RDG wille emit an interceptor (we produce an endpoint model with no diagnostics).
@@ -23,6 +23,7 @@ public EndpointParameter(Endpoint endpoint, IParameterSymbol parameter, WellKnow | |||
{ | |||
Ordinal = parameter.Ordinal; | |||
IsOptional = parameter.IsOptional(); | |||
HasDefaultValue = parameter.HasExplicitDefaultValue; |
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.
We add tracking for whether or not a method has a default parameter so that we can pass it in the concrete delegate type to support default parameter values in lambdas.
@@ -610,17 +613,17 @@ private static string ConvertEndOfLineAndQuotationCharactersToEscapeForm(string | |||
other.SymbolName == SymbolName && | |||
other.Ordinal == Ordinal && | |||
other.IsOptional == IsOptional && | |||
SymbolEqualityComparer.Default.Equals(other.Type, Type); | |||
SymbolEqualityComparer.IncludeNullability.Equals(other.Type, Type); |
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.
Prior to the interceptors feature, we couldn't generate separate overloads for endpoints that had the same parameter type but differing nullability (see #46622). Interceptors don't have the same problem so we can generate code for endpoints that use the same type but differing nullability.
@@ -167,11 +169,14 @@ internal Endpoint[] GetEndpointsFromCompilation(Compilation compilation, bool? e | |||
|
|||
foreach (var endpoint in endpoints) | |||
{ | |||
var sourceKeyMetadata = endpoint.Metadata.FirstOrDefault(metadata => metadata.GetType() == sourceKeyType); | |||
var sourceKeyMetadata = endpoint.Metadata.OfType<GeneratedCodeAttribute>().SingleOrDefault(); |
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.
We emitted the SourceKey
originally as a proactive measure to allow other possibly generated code (like the validation generator) to do look-ups on generated endpoints by the source location. Since we don't really need this for .NET 8 (we're not shipping other minimal API-related generators), I'm removing this type from the generated code and reusing the GeneratedCode
attribute to serve as a marker for when the endpoint is statically generated.
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.
I asked this in my review below, but I don't fully understand why we are doing this. Why do we need this marker?
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.
It helps us determine at runtime if an endpoint was generated at compile-time or runtime. For example, for determining what generator version an endpoint in a library was generated with compared to the app code consuming it.
We typically populate all attributes that are on an endpoint into its metadata so this makes sense to me assuming something like this:
app.MapGet("/", [GeneratedCode] (string name) => ...);
At runtime, we can get resolve if the endpoint was generated:
endpoint.Metadata.OfType<GeneratedCodeAttribute>();
d45c8f7
to
7b491a9
Compare
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.
I was able to integrate validations for the DiagnosticSuppressor
in our existing test infrastructure in https://github.com/dotnet/aspnetcore/tree/safia/interceptors-rdg-save. The changes work locally but it is challenging to get them to work in a Helix environment since we have to upload a version of the linker analyzer assembly onto the Helix machine along with the tests.
To spare us the MSBuild-foo, it should be sufficient to add E2E validation for the suppressor in the SDK repo. The setup in the branch above can also be used to validate the changeset locally.
...xtensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt
Outdated
Show resolved
Hide resolved
...xtensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt
Outdated
Show resolved
Hide resolved
{ | ||
Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); | ||
Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); | ||
options.EndpointBuilder.Metadata.Add(new System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.AspNetCore.Http.RequestDelegateGenerator, Version=42.42.42.42, Culture=neutral, PublicKeyToken=adb9793829ddae60", "42.42.42.42")); |
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.
Is this really necessary to add to the Metadata? What's its purpose?
{ | ||
httpContext.Response.StatusCode = 400; | ||
} | ||
|
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.
Why doesn't this just happen inline? It may make sense to throw a comment in the generated code...
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.
The parameter binding logic that sets the wasParamCheckFailure
is shared between the non-filter and filter-handler. However, the way we react to it is different depending on whether we are executing an endpoint with filters or not.
When no filters are registered, we return from the RequestDelegate immediately. When filters are registered, we don't return because we want the filters to run, but not the request delegate. This is easier to do if the check doesn't happen inline.
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.
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.
Why is the Cast helper method necessary? Why can't we just cast inline without using this method?
Ah, that part is in reaction to a breaking change in the compiler. Method group references are now inferred as anonymous delegate types instead of Action
/Func
to match the conversion for lambdas. This is the workaround @jjonescz and I came up with given the new constraint.
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.
Cast is more like Infer. Using generics to reference and unspeakable type (like an anonymous class).
{ | ||
%GENERATEDCODEATTRIBUTE% | ||
internal sealed class SourceKey | ||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] | ||
file sealed class InterceptsLocationAttribute : Attribute |
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.
What happens if another source generator adds this same file sealed class InterceptsLocationAttribute
to the project? For example - if I enable both RDG and the ConfigurationBinder source generator.
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.
Since the type is file scoped, it'll be fine to have multiple definitions in each generator as each will produce a unique type definition (e.g. System.Runtime.CompilerServices.<GeneratedFileName_g>FBAC_InterceptsLocationAttribute
).
Things will get interesting if generators aren't cautious about making them file private (as opposed to say internal) but we should fine here.
cc: @RikkiGibson At minimum, we probably want to document the requirement for InterceptsLocationAttribute
definitions to be file private. Not sure if we can add a compiler check/analyzer as well.
...estDelegateGenerator/Baselines/MapAction_MultipleSpecialTypeParam_StringReturn.generated.txt
Outdated
Show resolved
Hide resolved
namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator; | ||
|
||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
public sealed class RequestDelegateGeneratorSuppressor : DiagnosticSuppressor |
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.
More of a question because it's the first time I've seen these in action. Does a DiagnosticSuppressor need to be registered explicitly anywhere, or does its mere reference in the dependency graph kick it into action?
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.
Another thought do we need to write a test here to verify that this suppressor doesn't supress things that it shouldn't?
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.
More of a question because it's the first time I've seen these in action. Does a DiagnosticSuppressor need to be registered explicitly anywhere, or does its mere reference in the dependency graph kick it into action?
It doesn't need to be registered. Since it is annotated with the DiagnosticAnalyzer
attribute, the compiler infrastructure will pick it up automatically.
Another thought do we need to write a test here to verify that this suppressor doesn't supress things that it shouldn't?
Copying my response from another thread:
So, I was able to get this working with the automated tests in https://github.com/dotnet/aspnetcore/blob/safia/interceptors-rdg-save/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs#L96-L100.
One of the reasons I didn't pursue this testing strategy in the actual PR is because it's a bit of a pain to do a lookup for the linker analyzer assembly and then upload it to Helix, especially because the test projects themselves don't take a dependency on the linker analyzer.
The current proposal is to add tests for this E2E behavior in the SDK, where all the components already exist, so I'll make a point of adding a test to validate the true negative scenario there.
But TL;DR, the results I saw with the test infra locally gives me confidence about diagnostics being emitted for true negatives like
Lines 66 to 67 in 4870eea
public async Task BuildRequestDelegateEmitsDiagnosticForInvalidParameterListConstructor( | |
string parameterType, |
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.
This is really nice! The size of the PR doesn't do justice to the level of effort that went into this (I guess that is a sign that the interceptors stuff really simplifies things!).
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs
Outdated
Show resolved
Hide resolved
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs
Outdated
Show resolved
Hide resolved
....Extensions/test/RequestDelegateGenerator/Baselines/VerifyAsParametersBaseline.generated.txt
Outdated
Show resolved
Hide resolved
[InterceptsLocation(@"TestMapActions.cs", 25, 13)] | ||
[InterceptsLocation(@"OtherTestMapActions.cs", 25, 13)] | ||
internal static RouteHandlerBuilder MapGet_2513( |
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.
What happens if the calls come from different line and column numbers? Do we still merge them into 1 method?
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.
If the calls have the same exact signature, then yes. I can add this to the test case.
using Microsoft.CodeAnalysis.Operations; | ||
|
||
/* | ||
* This class contains the logic for suppressing diagnostics that are |
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.
(nit) why isn't this comment in a /// <summary>
on the class?
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.
I believe that generally in the repo we use code comments for dev-focused comments and /// <summary>
tags for things that should surface in user facing docs.
Even though it doesn't matter for this class since it doesn't ship in a library (and docs won't surface on the docs.microsoft.com site anyways), I kept the pattern.
2d526b2
to
4997fc0
Compare
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.
Looks good. Nice work, @captainsafia!
@@ -245,7 +245,7 @@ | |||
<Analyzer_MicrosoftCodeAnalysisCSharpWorkspacesVersion>3.3.1</Analyzer_MicrosoftCodeAnalysisCSharpWorkspacesVersion> | |||
<!-- Pin the version of the M.CA dependencies that we utilize with a cutom version property $(MicrosoftCodeAnalysisVersion_LatestVS) to avoid automatically | |||
consuming the newest version of the packages when using the $(MicrosoftCodeAnalysisCSharpVersion) properties in source-build. --> | |||
<MicrosoftCodeAnalysisVersion_LatestVS>4.5.0</MicrosoftCodeAnalysisVersion_LatestVS> | |||
<MicrosoftCodeAnalysisVersion_LatestVS>4.7.0-3.23314.3</MicrosoftCodeAnalysisVersion_LatestVS> |
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.
Was this just missed in #49245?
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.
Yes 😅
The other PR has already flown into the installer without any major issues so this hopefully doesn't cause any headaches....
Closes #48383
Closes #48289