From 543850363b5af413ed10cb7643b57665bc5f73aa Mon Sep 17 00:00:00 2001 From: Lucas Trzesniewski Date: Thu, 30 Nov 2023 00:14:41 +0100 Subject: [PATCH] Extend async check to sections --- .../RazorBladeSourceGeneratorTests.cs | 13 +++ ...TestNamespace.TestFile.Razor.g.verified.cs | 54 ++++++++++++ ...amespace.TestFile.RazorBlade.g.verified.cs | 22 +++++ .../LibraryCodeGenerator.cs | 84 +++++++++++++++---- .../Support/Extensions.cs | 12 +++ 5 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 src/RazorBlade.Analyzers.Tests/RazorBladeSourceGeneratorTests.should_detect_async_sections#TestNamespace.TestFile.Razor.g.verified.cs create mode 100644 src/RazorBlade.Analyzers.Tests/RazorBladeSourceGeneratorTests.should_detect_async_sections#TestNamespace.TestFile.RazorBlade.g.verified.cs diff --git a/src/RazorBlade.Analyzers.Tests/RazorBladeSourceGeneratorTests.cs b/src/RazorBlade.Analyzers.Tests/RazorBladeSourceGeneratorTests.cs index d1b65dd..6088f57 100644 --- a/src/RazorBlade.Analyzers.Tests/RazorBladeSourceGeneratorTests.cs +++ b/src/RazorBlade.Analyzers.Tests/RazorBladeSourceGeneratorTests.cs @@ -337,6 +337,19 @@ After section ); } + [Test] + public Task should_detect_async_sections() + { + return Verify( + """ + @using System.Threading.Tasks + @if(42.ToString() == "42") { + @section SectionName { @await Task.FromResult(42) } + } + """ + ); + } + private static GeneratorDriverRunResult Generate(string input, string? csharpCode, bool embeddedLibrary, diff --git a/src/RazorBlade.Analyzers.Tests/RazorBladeSourceGeneratorTests.should_detect_async_sections#TestNamespace.TestFile.Razor.g.verified.cs b/src/RazorBlade.Analyzers.Tests/RazorBladeSourceGeneratorTests.should_detect_async_sections#TestNamespace.TestFile.Razor.g.verified.cs new file mode 100644 index 0000000..4d3be74 --- /dev/null +++ b/src/RazorBlade.Analyzers.Tests/RazorBladeSourceGeneratorTests.should_detect_async_sections#TestNamespace.TestFile.Razor.g.verified.cs @@ -0,0 +1,54 @@ +//HintName: TestNamespace.TestFile.Razor.g.cs +#pragma checksum "./TestFile.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "ad80bc3dc0df64bf01f11639ab5672ca37eb6742" +// +#pragma warning disable 1591 +namespace TestNamespace +{ + #line hidden +#nullable restore +#line 1 "./TestFile.cshtml" +using System.Threading.Tasks; + +#line default +#line hidden +#nullable disable + #nullable restore + internal partial class TestFile : global::RazorBlade.HtmlTemplate + #nullable disable + { + #pragma warning disable 1998 + protected async override global::System.Threading.Tasks.Task ExecuteAsync() + { +#nullable restore +#line 2 "./TestFile.cshtml" + if(42.ToString() == "42") { + + +#line default +#line hidden +#nullable disable + DefineSection("SectionName", async() => { + WriteLiteral(" "); +#nullable restore +#line (3,29)-(3,54) 6 "./TestFile.cshtml" +Write(await Task.FromResult(42)); + +#line default +#line hidden +#nullable disable + WriteLiteral(" "); + } + ); +#nullable restore +#line 3 "./TestFile.cshtml" + +} + +#line default +#line hidden +#nullable disable + } + #pragma warning restore 1998 + } +} +#pragma warning restore 1591 diff --git a/src/RazorBlade.Analyzers.Tests/RazorBladeSourceGeneratorTests.should_detect_async_sections#TestNamespace.TestFile.RazorBlade.g.verified.cs b/src/RazorBlade.Analyzers.Tests/RazorBladeSourceGeneratorTests.should_detect_async_sections#TestNamespace.TestFile.RazorBlade.g.verified.cs new file mode 100644 index 0000000..9ba802b --- /dev/null +++ b/src/RazorBlade.Analyzers.Tests/RazorBladeSourceGeneratorTests.should_detect_async_sections#TestNamespace.TestFile.RazorBlade.g.verified.cs @@ -0,0 +1,22 @@ +//HintName: TestNamespace.TestFile.RazorBlade.g.cs +// + +#nullable restore + +namespace TestNamespace +{ + partial class TestFile + { + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("The generated template is async. Use RenderAsync instead.", DiagnosticId = "RB0003")] + public new string Render(global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + => base.Render(cancellationToken); + + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("The generated template is async. Use RenderAsync instead.", DiagnosticId = "RB0003")] + public new void Render(global::System.IO.TextWriter textWriter, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + => base.Render(textWriter, cancellationToken); + } +} diff --git a/src/RazorBlade.Analyzers/LibraryCodeGenerator.cs b/src/RazorBlade.Analyzers/LibraryCodeGenerator.cs index 27e69b6..18f9493 100644 --- a/src/RazorBlade.Analyzers/LibraryCodeGenerator.cs +++ b/src/RazorBlade.Analyzers/LibraryCodeGenerator.cs @@ -52,6 +52,8 @@ private static readonly SymbolDisplayFormat _paramFootprintFormat private INamedTypeSymbol? _classSymbol; private ImmutableArray _diagnostics; private Compilation _compilation; + private SemanticModel? _semanticModel; + private ClassDeclarationSyntax? _classDeclarationSyntax; public LibraryCodeGenerator(RazorCSharpDocument generatedDoc, Compilation compilation, @@ -86,7 +88,7 @@ public string Generate(CancellationToken cancellationToken) using (_writer.BuildClassDeclaration(["partial"], _classSymbol.Name, null, Array.Empty(), Array.Empty(), useNullableContext: false)) { GenerateConstructors(); - GenerateConditionalOnAsync(); + GenerateConditionalOnAsync(cancellationToken); } } @@ -107,17 +109,17 @@ private void Analyze(CancellationToken cancellationToken) .AddSyntaxTrees(syntaxTree) .AddSyntaxTrees(_additionalSyntaxTrees); - var semanticModel = _compilation.GetSemanticModel(syntaxTree); + _semanticModel = _compilation.GetSemanticModel(syntaxTree); - var classDeclarationNode = syntaxTree.GetRoot(cancellationToken) - .DescendantNodes() - .FirstOrDefault(static i => i.IsKind(SyntaxKind.ClassDeclaration)); + _classDeclarationSyntax = syntaxTree.GetRoot(cancellationToken) + .DescendantNodes() + .FirstOrDefault(static i => i.IsKind(SyntaxKind.ClassDeclaration)) as ClassDeclarationSyntax; - _classSymbol = classDeclarationNode is ClassDeclarationSyntax classDeclarationSyntax - ? semanticModel.GetDeclaredSymbol(classDeclarationSyntax, cancellationToken) + _classSymbol = _classDeclarationSyntax is not null + ? _semanticModel.GetDeclaredSymbol(_classDeclarationSyntax, cancellationToken) : null; - _diagnostics = semanticModel.GetDiagnostics(cancellationToken: cancellationToken); + _diagnostics = _semanticModel.GetDiagnostics(cancellationToken: cancellationToken); } private void GenerateConstructors() @@ -164,26 +166,29 @@ private void GenerateConstructors() } } - private void GenerateConditionalOnAsync() + private void GenerateConditionalOnAsync(CancellationToken cancellationToken) { + const string executeAsyncMethodName = "ExecuteAsync"; + const string defineSectionMethodName = "DefineSection"; + var conditionalOnAsyncAttribute = _compilation.GetTypeByMetadataName("RazorBlade.Support.ConditionalOnAsyncAttribute"); if (conditionalOnAsyncAttribute is null) return; - var executeMethodSymbol = _classSymbol?.GetMembers("ExecuteAsync") - .OfType() - .FirstOrDefault(i => i.Parameters.IsEmpty && i.IsAsync); + var executeMethodSyntax = _classDeclarationSyntax?.ChildNodes() + .Where(m => m.IsKind(SyntaxKind.MethodDeclaration)) + .OfType() + .FirstOrDefault(m => m.Identifier.ValueText == executeAsyncMethodName + && m.Modifiers.Any(SyntaxKind.AsyncKeyword) + && m.ParameterList.Parameters.Count == 0); - var methodLocation = executeMethodSymbol?.Locations.FirstOrDefault(); - if (methodLocation is null) + if (executeMethodSyntax is null) return; - // CS1998 = This async method lacks 'await' operators and will run synchronously. - var isTemplateSync = _diagnostics.Any(i => i.Id == "CS1998" && i.Location == methodLocation); - + var isTemplateSync = IsTemplateSync(); var hiddenMethodSignatures = new HashSet(StringComparer.Ordinal); - for (var baseClass = _classSymbol?.BaseType; baseClass is not (null or { SpecialType: SpecialType.System_Object }); baseClass = baseClass.BaseType) + foreach (var baseClass in _classSymbol.SelfAndBasesTypes().Skip(1)) { foreach (var methodSymbol in baseClass.GetMembers().OfType()) { @@ -233,6 +238,49 @@ private void GenerateConditionalOnAsync() } } + bool IsTemplateSync() + { + // CS1998 = This async method lacks 'await' operators and will run synchronously. + // The ExecuteAsync and all the DefineSection methods need to have this diagnostic for the template to be considered synchronous. + + var diagnosticLocations = _diagnostics.Where(i => i.Id == "CS1998").Select(i => i.Location).ToHashSet(); + if (!diagnosticLocations.Contains(executeMethodSyntax.Identifier.GetLocation())) + return false; + + var defineSectionMethod = _classSymbol.SelfAndBasesTypes() + .SelectMany(t => t.GetMembers(defineSectionMethodName)) + .OfType() + .FirstOrDefault(m => m.Parameters is + [ + { Type.SpecialType: SpecialType.System_String }, + { Type.TypeKind: TypeKind.Delegate } + ]); + + if (defineSectionMethod is null || executeMethodSyntax.Body is not { } executeMethodBody) + return true; + + foreach (var node in executeMethodBody.DescendantNodes()) + { + if (node is InvocationExpressionSyntax + { + ArgumentList.Arguments: + [ + { Expression: LiteralExpressionSyntax { RawKind: (int)SyntaxKind.StringLiteralExpression } }, + { Expression: ParenthesizedLambdaExpressionSyntax { AsyncKeyword.RawKind: (int)SyntaxKind.AsyncKeyword } lambda } + ], + Expression: IdentifierNameSyntax { Identifier.ValueText: defineSectionMethodName } expression + } + && !diagnosticLocations.Contains(lambda.ArrowToken.GetLocation()) + && SymbolEqualityComparer.Default.Equals(_semanticModel.GetSymbolInfo(expression, cancellationToken).Symbol, defineSectionMethod) + ) + { + return false; + } + } + + return true; + } + static string GetMethodSignatureFootprint(IMethodSymbol methodSymbol) { var sb = new StringBuilder(); diff --git a/src/RazorBlade.Analyzers/Support/Extensions.cs b/src/RazorBlade.Analyzers/Support/Extensions.cs index 9b37bb2..6343845 100644 --- a/src/RazorBlade.Analyzers/Support/Extensions.cs +++ b/src/RazorBlade.Analyzers/Support/Extensions.cs @@ -7,6 +7,9 @@ namespace RazorBlade.Analyzers.Support; internal static class Extensions { + public static HashSet ToHashSet(this IEnumerable items) + => new(items); + public static IncrementalValuesProvider WhereNotNull(this IncrementalValuesProvider provider) where T : class => provider.Where(static item => item is not null)!; @@ -33,6 +36,15 @@ public static string EscapeCSharpKeyword(this string name) ? "@" + name : name; + public static IEnumerable SelfAndBasesTypes(this INamedTypeSymbol? symbol) + { + while (symbol is not null) + { + yield return symbol; + symbol = symbol.BaseType; + } + } + private sealed class LambdaComparer(Func equals, Func getHashCode) : IEqualityComparer { public bool Equals(T? x, T? y)