diff --git a/analyzers/rspec/cs/S6802.html b/analyzers/rspec/cs/S6802.html index 35fb99e84c2..698b7718b85 100644 --- a/analyzers/rspec/cs/S6802.html +++ b/analyzers/rspec/cs/S6802.html @@ -5,8 +5,13 @@

Why is this an issue?

elements.

The reason behind this is that Blazor rebuilds all lambda expressions within the loop every time the UI elements are rendered.

How to fix it

-

Ensure to not use a delegate in elements rendered in loops by using a collection of objects containing the delegate as an Action.

+

Ensure to not use a delegate in elements rendered in loops, you can try:

+

Code examples

Noncompliant code example

@@ -15,7 +20,7 @@ 

Noncompliant code example

var buttonNumber = i; <button @onclick="@(e => DoAction(e, buttonNumber))"> @* Noncompliant *@ - Button #buttonNumber + Button #@buttonNumber </button> } @@ -62,6 +67,67 @@

Compliant solution

} }
+

Noncompliant code example

+
+@* Component.razor *@
+
+@for (var i = 1; i < 100; i++)
+{
+    var buttonNumber = i;
+
+    <button @onclick="@(e => DoAction(e, buttonNumber))"> @* Noncompliant *@
+        Button #@buttonNumber
+    </button>
+}
+
+@code {
+    private void DoAction(MouseEventArgs e, int button)
+    {
+        // Do something here
+    }
+}
+
+

Compliant solution

+
+@* MyButton.razor *@
+
+<button @onclick="OnClickCallback">
+    @ChildContent
+</button>
+
+@code {
+    [Parameter]
+    public int Id { get; set; }
+
+    [Parameter]
+    public EventCallback<int> OnClick { get; set; }
+
+    [Parameter]
+    public RenderFragment ChildContent { get; set; }
+
+    private void OnClickCallback()
+    {
+        OnClick.InvokeAsync(Id);
+    }
+}
+
+@* Component.razor *@
+
+@for (var i = 1; i < 100; i++)
+{
+    var buttonNumber = i;
+    <MyButton Id="buttonNumber" OnClick="DoAction">
+        Button #@buttonNumber
+    </MyButton>
+}
+
+@code {
+    private void DoAction(int button)
+    {
+        // Do something here
+    }
+}
+

Resources

Documentation

Benchmarks

The results were generated with the help of BenchmarkDotNet and { var node = (LambdaExpressionSyntax)c.Node; - if (IsWithinLoop(node) + if (IsWithinLoopBody(node) && IsWithinRenderTreeBuilderInvocation(node, c.SemanticModel)) { c.ReportIssue(Diagnostic.Create(Rule, node.GetLocation())); @@ -61,6 +61,6 @@ private static bool IsWithinRenderTreeBuilderInvocation(SyntaxNode node, Semanti && semanticModel.GetSymbolInfo(invocation.Expression).Symbol is IMethodSymbol symbol && symbol.ContainingType.GetSymbolType().Is(KnownType.Microsoft_AspNetCore_Components_Rendering_RenderTreeBuilder)); - private static bool IsWithinLoop(SyntaxNode node) => - node.AncestorsAndSelf().Any(x => x is ForStatementSyntax or ForEachStatementSyntax or WhileStatementSyntax or DoStatementSyntax); + private static bool IsWithinLoopBody(SyntaxNode node) => + node.AncestorsAndSelf().Any(x => x is BlockSyntax && x.Parent is ForStatementSyntax or ForEachStatementSyntax or WhileStatementSyntax or DoStatementSyntax); } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/AvoidLambdaExpressionInLoopsInBlazorTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/AvoidLambdaExpressionInLoopsInBlazorTest.cs index 5b148f2fcb4..ee26e6e855a 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Rules/AvoidLambdaExpressionInLoopsInBlazorTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Rules/AvoidLambdaExpressionInLoopsInBlazorTest.cs @@ -37,6 +37,12 @@ public void AvoidLambdaExpressionInLoopsInBlazor_Blazor() => .WithAdditionalFilePath(AnalysisScaffolding.CreateSonarProjectConfig(TestContext, ProjectType.Product)) .Verify(); + [TestMethod] + public void AvoidLambdaExpressionInLoopsInBlazor_BlazorLoopsWithNoBody() => + builder.AddPaths("AvoidLambdaExpressionInLoopsInBlazor.LoopsWithNoBody.razor") + .WithAdditionalFilePath(AnalysisScaffolding.CreateSonarProjectConfig(TestContext, ProjectType.Product)) + .Verify(); + [TestMethod] public void AvoidLambdaExpressionInLoopsInBlazor_UsingRenderFragment() => builder.AddPaths("AvoidLambdaExpressionInLoopsInBlazor.RenderFragment.razor", "AvoidLambdaExpressionInLoopsInBlazor.RenderFragmentConsumer.razor") diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AvoidLambdaExpressionInLoopsInBlazor.LoopsWithNoBody.razor b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AvoidLambdaExpressionInLoopsInBlazor.LoopsWithNoBody.razor new file mode 100644 index 00000000000..bf51604d39f --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AvoidLambdaExpressionInLoopsInBlazor.LoopsWithNoBody.razor @@ -0,0 +1,102 @@ +@* https://github.com/SonarSource/sonar-dotnet/issues/8394 *@ + +@foreach (var item in Buttons.Where(x => x.Id == "idToFind")) + if (item.Id == "idToFind") + { + @* FN *@ + } + +@for (int i = 0; i < Buttons.Count; i++) + @if (i % 2 == 0) + { + var buttonNumber = i; + + } + +@{ + var j = 0; + while (j < 5) + if (j % 2 == 0) + { + j += 2; + + j += 2; + } + + do + if (j % 2 == 0) + { + + j += 2; + } + while (j < 10); +} + +@foreach (var item in Buttons.Where(x => x.Id == "idToFind")) + @if (item.Id == "idToFind") + { + @* FN *@ + } + else if (item.Id == "idToFind") + { + @* FN *@ + } + else + { + @* FN *@ + } + +@foreach (var item in Buttons.Where(x => x.Id == "idToFind")) + @if (true) + @if (item.Id == "idToFind") + { + @* FN *@ + } + +@foreach (var item in Buttons.Where(x => x.Id == "idToFind")) + @switch(item.Id) + { + case "idToFind": + @* FN *@ + break; + default: + { + @* FN *@ + break; + } + } + +@foreach (var button in Buttons) +{ + { + + } +} + +@code { + private List } + + @foreach (var button in Buttons.Where(x => x.Id == "SomeId")) @* Compliant *@ + { + + } @code { diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AvoidLambdaExpressionInLoopsInBlazor.cs b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AvoidLambdaExpressionInLoopsInBlazor.cs index 9e8d7c9b04d..c5280552bdc 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AvoidLambdaExpressionInLoopsInBlazor.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AvoidLambdaExpressionInLoopsInBlazor.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Web; using System; +using System.Linq; using System.Collections.Generic; class LambdaInLoopInMethod @@ -77,5 +78,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddMarkupContent(14, "\r\n Button"); builder.CloseElement(); } + + foreach (var button in Buttons.OrderByDescending(x => x.Id)) { } // Compliant, the lambda is executed outside of the loop } } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AvoidLambdaExpressionInLoopsInBlazor.razor b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AvoidLambdaExpressionInLoopsInBlazor.razor index 63d79383d6c..dc9a54898d9 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AvoidLambdaExpressionInLoopsInBlazor.razor +++ b/analyzers/tests/SonarAnalyzer.UnitTest/TestCases/AvoidLambdaExpressionInLoopsInBlazor.razor @@ -46,6 +46,11 @@ @* Compliant *@ } +@foreach (var button in Buttons.OrderByDescending(x => x.Id)) @* Compliant, the lambda is executed outside of the loop *@ +{ +

@button.Id

+} + @code { private List