Skip to content

Add analyzer to prevent local functions accessing RenderTreeBuilder from parent scope in Blazor components - #63826

Open
javiercn with Copilot wants to merge 16 commits into
mainfrom
copilot/fix-786077d2-7ae1-4997-8907-79fb54489d2a
Open

Add analyzer to prevent local functions accessing RenderTreeBuilder from parent scope in Blazor components#63826
javiercn with Copilot wants to merge 16 commits into
mainfrom
copilot/fix-786077d2-7ae1-4997-8907-79fb54489d2a

Conversation

Copilot AI commented Sep 25, 2025

Copy link
Copy Markdown
Contributor

Description

This PR adds a new analyzer diagnostic ASP0039 that detects local functions which access RenderTreeBuilder methods from captured variables in their parent scope within Blazor components.

Problem

Local functions defined within @{ ... } blocks in Razor components' BuildRenderTree methods can capture RenderTreeBuilder instances from their parent scope, leading to incorrect rendering behavior. This pattern appears to work but actually corrupts the rendering output instead of properly writing to child component render fragments.

@{
    void RenderTree(int depth, int maxDepth)
    {
        if (depth >= maxDepth) return;
        
        <FluentTreeItem Text="item">
            @{ RenderTree(depth + 1, maxDepth); }  // ❌ Uses wrong builder
        </FluentTreeItem>
    }
}

The issue occurs because C# scoping rules cause the local function to capture the RenderTreeBuilder from the parent context rather than using the builder that should be passed to the RenderFragment.

Solution

The analyzer is efficiently scoped to only analyze local functions within:

  • BuildRenderTree methods (by name)
  • Classes that extend ComponentBase

This scoping prevents false positives in non-component code and focuses analysis on the actual problematic pattern in Razor components.

New Diagnostic: ASP0039

  • Severity: Error
  • Category: Usage
  • Message: "Local function '{functionName}' accesses RenderTreeBuilder from parent scope, which can cause incorrect rendering behavior. Consider making it a static method or regular instance method that takes RenderTreeBuilder as a parameter."

Detection Logic

The analyzer intelligently identifies problematic patterns while allowing safe alternatives:

Allowed (Safe Patterns):

  • Static local functions (cannot capture from parent scope)
  • Local functions that take RenderTreeBuilder as a parameter
  • Local functions outside of ComponentBase-derived classes
  • Local functions in methods other than BuildRenderTree
  • Local functions that don't use RenderTreeBuilder at all

Detected (Problematic Patterns):

  • Local functions accessing RenderTreeBuilder from captured variables within BuildRenderTree methods

Example

Before (causes runtime issues):

public class MyComponent : ComponentBase
{
    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        void LocalFunction()  // ❌ ASP0039: Captures builder from parent scope
        {
            builder.OpenElement(0, "div");
            builder.CloseElement();
        }
        
        LocalFunction();
    }
}

After (recommended approaches):

public class MyComponent : ComponentBase
{
    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        // Option 1: Static local function with parameter
        static void LocalFunction(RenderTreeBuilder builderParam)  // ✅ Safe
        {
            builderParam.OpenElement(0, "div");
            builderParam.CloseElement();
        }
        
        LocalFunction(builder);
    }
}

// Option 2: Regular method
public class MyComponent : ComponentBase
{
    public RenderFragment CreateFragment() => builder =>  // ✅ Safe
    {
        builder.OpenElement(0, "div");
        builder.CloseElement();
    };
}

Testing

Added comprehensive test coverage across 40 targeted ASP0039 cases covering:

  • Local functions with captured RenderTreeBuilder in BuildRenderTree (detected)
  • Static local functions (allowed)
  • Local functions with RenderTreeBuilder parameters (allowed)
  • Nested local function scenarios
  • Local functions without RenderTreeBuilder usage (allowed)
  • Local functions outside of ComponentBase classes (not analyzed)
  • Local functions in non-BuildRenderTree methods (not analyzed)

All 751 analyzer tests pass, including 40 targeted ASP0039 cases.

Performance

By scoping the analysis to only BuildRenderTree methods in ComponentBase-derived classes (rather than all local functions), the analyzer is much more efficient and avoids false positives in non-component code.

Impact

This change helps developers avoid a subtle but problematic pattern that can cause rendering corruption in Blazor applications. The analyzer provides clear, actionable feedback at compile time rather than allowing runtime failures.

Original prompt

This section details on the original issue you should resolve

<issue_title>Prevent use of local functions inside markup</issue_title>
<issue_description>[Edit by @SteveSandersonMS] This issue was originally reported by @verdie-g as follows below the line. On investigation the problem is that C# has added a new syntax that doesn't work in Razor.

The Razor compiler allows arbitrary C# code within @{ ... } blocks. Unfortunately this means it allows the use of local functions in a way that confuses the parsing logic, causing it to use the wrong __builder instance. Example:

<FluentTreeView>
@{
    RenderTree(0, 3);

    void RenderTree(int depth, int maxDepth)
    {
        if (depth >= maxDepth)
        {
            return;
        }

        <FluentTreeItem Text="item">
            @{ RenderTree(depth + 1, maxDepth); }
        </FluentTreeItem>
    }
}
</FluentTreeView>

Here, the child content of FluentTreeItem should be compiled as a RenderFragment that acts on whatever RenderTreeBuilder is passed in. But because of C# scoping rules, the RenderFragment actually acts on the __builder captured from its parent context, so it is simply corrupting the output instead of doing something useful.

Possible solutions:

  1. We could ask for the Razor compiler block the use of local functions inside @{ ... } specifically. However that's probably impractical because Razor doesn't parse the contents of @{ ... }.
    • Perhaps it is achievable as an analyzer that acts on the code after the Razor compiler has generated it.
  2. We could do something in the runtime to detect more generally any cases where the wrong RenderTreeBuilder is invoked. For example if the runtime set an "rendering in progress" flag on it before it starts rendering and synchronously unsets that flag at the end of rendering, then it would have caught this case because child components are rendered afterwards (not recursively), so when the child is rendered it would see it's trying to write to a builder that does not have the "rendering in progress" flag set.
    • Drawback: how do we even check if this flag is set? We would not check it as part of each rendering instruction. There's plenty of evidence that rendering perf is sensitive to that kind of thing (and we can't just check it in development either).
    • Possible solution: instead of just setting a flag, actually null out the referencing to the underlying buffer (storing it in some other field to be swapped back later). Then if anyone tries to write to the builder while it's not marked as rendering-in-progress, they will get a NullReferenceException instead of corrupt output. Obviously that's not super easy to understand but avoids any perf cost.

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I'm rendering a blazor wasm component using a recursive C# method and while it's working fine using C# only (OpenComponent, AddAttribute, etc.), it fails when returning HTML from that recursive method.

Expected Behavior

I'm expecting a tree structure to be built and clicking on a line should expand its children but it seems like the children are not rendered and an error is thrown on click.

Steps To Reproduce

I was not able to reproduce the issue without the library fluentui-blazor.

dotnet new install Microsoft.FluentUI.AspNetCore.Templates
dotnet new fluentblazorwasm --name aspnetcore-issue-53269
cd aspnetcore-issue-53269

Then replace Home.razor with

@page "/"

<PageTitle>Home</PageTitle>

<FluentTreeView>
@{
    RenderTree(0, 3);

    void RenderTree(int depth, int maxDepth)
    {
        if (depth >= maxDepth)
        {
            return;
        }

        <FluentTreeItem Text="item">
            @{ RenderTree(depth + 1, maxDepth); }
        </FluentTreeItem>
    }
}
</FluentTreeView>

Click on the item generated and check the error in the console.

It could be an error with the library (initially reported here microsoft/fluentui-blazor#1289) but this code works fine:

@page "/"

<PageTitle>Home</PageTitle>

<FluentTreeView>
    @RenderTree(0, 3)
</FluentTreeView>

@code {
    public static RenderFragment RenderTree(int depth, int maxDepth)
    {
        return builder =>
        {
            if (depth >= maxDepth)
            {
                return;
            }

            builder.OpenComponent<FluentTreeItem>(0);
            builder.AddAttribute(1, "Text", "item");
            builder.AddAttribute(2, "ChildContent", RenderTree(depth + 1, maxDepth));
            builder.CloseComponent();
        };
    }
}

Exceptions (if any)

blazor.webassembly.js:1 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRendere...

</details>
Fixes dotnet/aspnetcore#53269

<!-- START COPILOT CODING AGENT TIPS -->
---

✨ Let Copilot coding agent [set things up for you](https://github.com/dotnet/aspnetcore/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Greetings human! You've submitted a PR that modifies code that is shared with https://github.com/dotnet/runtime . Please make sure you synchronize this code with the changes in that repo!

Copilot AI and others added 2 commits September 25, 2025 12:19
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
…der analyzer

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copilot AI changed the title [WIP] Prevent use of local functions inside markup Add analyzer to prevent local functions accessing RenderTreeBuilder from parent scope Sep 25, 2025
Copilot AI requested a review from javiercn September 25, 2025 12:29
@javiercn javiercn added the area-blazor Includes: Blazor, Razor Components label Jan 4, 2026
Comment on lines +62 to +74
context.RegisterSyntaxNodeAction(context =>
{
var localFunction = (LocalFunctionStatementSyntax)context.Node;

// Check if this local function contains any RenderTreeBuilder method calls
if (ContainsRenderTreeBuilderCalls(wellKnownTypes, localFunction, context.SemanticModel))
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.DoNotUseLocalFunctionsInMarkup,
localFunction.Identifier.GetLocation(),
localFunction.Identifier.ValueText));
}
}, SyntaxKind.LocalFunctionStatement);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than analyze every single local function declaration, scope your analysis to components first. Then check only BuildRenderTree if the method extends component base.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored in commit 36e3eba. The analyzer now scopes analysis to BuildRenderTree methods within ComponentBase-derived classes, significantly improving efficiency and avoiding false positives in non-component code.

@javiercn

Copy link
Copy Markdown
Member

@copilot chop chop

…Base classes

Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Copilot AI changed the title Add analyzer to prevent local functions accessing RenderTreeBuilder from parent scope Add analyzer to prevent local functions accessing RenderTreeBuilder from parent scope in Blazor components Jan 26, 2026
Copilot AI requested a review from javiercn January 26, 2026 16:16
@PureWeen

Copy link
Copy Markdown
Member

@copilot Update this draft onto current main, resolve conflicts while preserving the ASP0029 analyzer intent, re-evaluate the implementation against today’s analyzer infrastructure, run the targeted analyzer tests, and summarize every conflict resolution. Keep the PR draft.

Copilot AI and others added 5 commits August 13, 2026 08:38
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>

Copilot AI commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Update this draft onto current main, resolve conflicts while preserving the ASP0029 analyzer intent, re-evaluate the implementation against today’s analyzer infrastructur...

Updated through d774c508ab; PR remains draft. Merged current main (704fddddc3). There was one merge conflict: DiagnosticDescriptors.cs. I preserved main’s descriptor factory/Learn help-link infrastructure and retained the new analyzer as ASP0039 because ASP0029–ASP0038 are now reserved. I also restored two stale submodule pointers to exactly match main, isolated generated-code analysis from ASP0006, fixed nested-lambda capture scoping, and added current diagnostics documentation. Targeted RenderTreeBuilder analyzer tests pass 20/20.

Copilot AI and others added 2 commits August 13, 2026 14:15
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
@PureWeen

Copy link
Copy Markdown
Member

@copilot Please revise the current draft at b654ce5ba22892328dc83e5dbcbc0bd0513c6e43. The provenance correction fixed the previously reported fresh-builder false positive and straight-line owning-alias false negative; all 22 existing corrected-head tests pass locally. A latest-head review found three remaining blockers.

  1. Restore clean CI reference resolution. The new raw NuGet-cache reference to microsoft.aspnetcore.razor.language/6.0.0 bypasses this repository's reference resolver and causes MSB3245 on Linux, Windows, and macOS before tests execute. Replace it with the repository-native symbolic reference:
<Reference Include="Microsoft.AspNetCore.Razor.Language" />

Do not add a direct PackageReference. The symbolic reference repairs the legacy in-process Razor test dependency, but do not describe it as current Razor SDK fidelity.

  1. Fix loop provenance soundly. The current walker loses the loop-entry owning state after visiting a possibly-zero-iteration loop. This produces no ASP0039 here even though alias may still be the owning builder:
var alias = builder;
while (ShouldReplace())
{
    alias = new RenderTreeBuilder();
}

void LocalFunction()
{
    alias.OpenElement(0, "div");
    alias.CloseElement();
}

The exact regression is red at the current head: 23 total, 22 pass, expected ASP0039 absent. A targeted loop-entry/body join makes it green and the wider analyzer project passes 734/734, but that proof patch is not a production solution: it mishandles do and leaves for, foreach, and general back edges incomplete. Implement conservative loop-family transfer/fixed-point behavior that distinguishes zero-or-more while/for/foreach paths from at-least-once do, while preserving definite assignments made by directly invoked local functions. Add opposite-side loop tests rather than copying the narrow proof patch.

  1. Use the real current Razor component producer for the issue test. The present RazorProjectEngine test is genuinely Razor-generated, but with no tag-helper/component descriptors it lowers <FluentTreeItem> as OpenElement and emits no OpenComponent or ChildContent. It therefore does not exercise the component/render-fragment producer from the issue. Add a small Microsoft.NET.Sdk.Razor test asset containing a real FluentTreeItem component and IssueSample.razor, enable generated files, and deliberately copy/embed the generated C# for the analyzer test. Assert component identity, ChildContent, recursive local-function shape, mapped .razor location, and ASP0039. Do not substitute <div> or checked-in generated-looking C#.

Preserve all existing fresh-builder, owning-alias, both reassignment directions, chained assignment, nested local/lambda, method-group, and real RenderFragment child-builder controls. Rerun the corrected test class, full analyzer project, and clean CI. Keep the PR draft, update its stale description from ASP0029/old test totals to ASP0039/current totals, and report exact results and remaining proof limits when complete.

@PureWeen

Copy link
Copy Markdown
Member

@copilot Please revise this draft at b654ce5ba22892328dc83e5dbcbc0bd0513c6e43.

The straight-line provenance fixes look good, but three issues remain:

  1. CI build 1552200 fails before tests run because the analyzer test project uses a raw NuGet-cache path for Microsoft.AspNetCore.Razor.Language. Use the repository-native symbolic reference instead.
  2. A possibly-zero-iteration loop loses owning-builder provenance. We reproduced this as a strict red/green failure. Add loop-aware provenance while preserving definite local-function assignments and do at-least-once semantics.
  3. The Razor test uses empty tag-helper descriptors and actually generates OpenElement("FluentTreeItem") with inline recursion, not OpenComponent/ChildContent. Please use an SDK-built Razor fixture with a real component so this covers the reported producer path.

Keep the existing alias, reassignment, nested local/lambda, method-group, and real RenderFragment controls. Run the ASP0039 class, full analyzer project, and clean CI. Also update the stale PR description from ASP0029 and the old test totals.

Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Copilot AI and others added 2 commits August 13, 2026 19:37
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Copilot stopped work on behalf of PureWeen due to an error August 13, 2026 20:07
@PureWeen

Copy link
Copy Markdown
Member

@copilot Please revise this draft at c4561303def3c2ffb361ce9b4b6336761207b24f.

The loop provenance and SDK Razor fixture changes look much better, but two blockers remain:

  1. LocalFunctionInRazorGeneratedBuildRenderTree_ProducesDiagnostic fails deterministically on Linux/macOS Helix. The generated #line path is Windows-formatted, so POSIX Path.GetFileName returns the full D:\a\...\IssueSample.razor path. Normalize both separators before extracting/comparing the filename.
  2. Return paths still contribute owning-builder provenance after they terminate. We reproduced this with strict red/green: the current head reported ASP0039 when alias = builder; return; was the only owning path, even though the later local function was reachable only with a fresh builder.

Please add the returning-branch no-diagnostic regression and scope _pathTerminated while visiting local-function and anonymous-function bodies. A return inside a callee/lambda must not terminate caller traversal, while direct local-function provenance assignments must still flow to the caller. We added both leakage guards in the proof; the corrected class passed 40/40 and the full analyzer project passed 751/751.

Keep the current straight-line, loop-family, switch, nested local/lambda, method-group, RenderFragment, and SDK Razor fixture controls. Don't broaden this pass into ad-hoc goto case or CFG work. If you include throw, make sure its direct-call and catch/finally semantics are tested rather than treating it exactly like return.

Please rerun the ASP0039 class, full analyzer project, and clean cross-platform CI. Also update the stale PR description from ASP0029 and the old test totals.

Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
@PureWeen
PureWeen marked this pull request as ready for review August 18, 2026 21:23
Copilot AI lite review requested due to automatic review settings August 18, 2026 21:23
@PureWeen
PureWeen requested a review from SamMonoRT as a code owner August 18, 2026 21:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new ASP.NET Core analyzer diagnostic (ASP0039) that flags local functions inside Blazor component BuildRenderTree(RenderTreeBuilder) overrides when they access a captured RenderTreeBuilder from an outer scope, which can corrupt rendering output in Razor-generated code.

Changes:

  • Adds the DoNotUseLocalFunctionsInMarkupAnalyzer (ASP0039) scoped to ComponentBase + BuildRenderTree(RenderTreeBuilder) overrides.
  • Adds analyzer tests, including coverage for Razor-generated output via a test asset project and copied generated .g.cs.
  • Updates diagnostic resources and the public diagnostics list to include ASP0039.
Show a summary per file
File Description
src/Framework/AspNetCoreAnalyzers/test/testassets/DoNotUseLocalFunctionsInMarkup/IssueSample.razor Adds a Razor repro sample that generates the problematic local-function pattern.
src/Framework/AspNetCoreAnalyzers/test/testassets/DoNotUseLocalFunctionsInMarkup/FluentTreeItem.razor Adds a minimal component used by the Razor repro sample.
src/Framework/AspNetCoreAnalyzers/test/testassets/DoNotUseLocalFunctionsInMarkup/DoNotUseLocalFunctionsInMarkup.csproj Adds a Razor SDK test asset project that emits compiler-generated files for inspection.
src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj Wires the test asset project and copies the generated .g.cs into test output; adds Razor.Language reference.
src/Framework/AspNetCoreAnalyzers/test/Components/DoNotUseLocalFunctionsInMarkupTest.cs Adds comprehensive analyzer test coverage, including generated Razor code mapping assertions.
src/Framework/AspNetCoreAnalyzers/test/Components/DisallowNonLiteralSequenceNumbersTest.cs Adds a regression-style test ensuring generated code scenarios work for existing analyzer infrastructure.
src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx Adds localized title/message strings for the new diagnostic.
src/Framework/AspNetCoreAnalyzers/src/Analyzers/RenderTreeBuilder/DoNotUseLocalFunctionsInMarkupAnalyzer.cs Implements the new analyzer with flow/provenance tracking to detect captured owning-builder usage.
src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs Registers ASP0039 diagnostic descriptor.
docs/list-of-diagnostics.md Updates the documented ASP diagnostic range and adds ASP0039 to the list.

Review details

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 10/10 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx
@PureWeen

Copy link
Copy Markdown
Member

@copilot Two small review follow-ups are valid at 0a53e1a6bf6f0329dff3be0ef5e1059395fdd5ff:

  1. Change Analyzer_DoNotUseLocalFunctionsInMarkup_Title from Do not use local functions in markup to Do not access a captured RenderTreeBuilder from a local function. That matches what ASP0039 actually reports and the existing diagnostics table.
  2. Refresh the PR description so every ASP0029 reference says ASP0039, and replace the stale 9/13 test totals with the current results (40 targeted ASP0039 cases and 751 analyzer tests). Current CI is green.

Please keep the PR ready for review and make only those resource/description updates. Don't add a null guard around Visit(operation.ReturnedValue): Roslyn's Visit(IOperation?) is already null-safe.

@PureWeen

Copy link
Copy Markdown
Member

@copilot Please pick up the two scoped follow-ups in #63826 (comment) on the current head. Only update the diagnostic title resource and PR description, keep the PR ready, and do not change VisitReturn.

Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>

Copilot AI commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Two small review follow-ups are valid at 0a53e1a6bf6f0329dff3be0ef5e1059395fdd5ff:

  1. Change Analyzer_DoNotUseLocalFunctionsInMarkup_Title from `Do not use local functio...

Updated the ASP0039 diagnostic title in afbb7375; the PR remains ready for review. The available PR tooling does not expose body editing, so the description still needs its ASP0029 references changed to ASP0039 and its totals updated to 40 targeted/751 analyzer tests.

Copilot AI commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Please pick up the two scoped follow-ups in #63826 (comment) on the current head. Only update the diagnostic title resource and PR descr...

Both scoped follow-ups are complete on current head afbb7375: the diagnostic title resource has the requested wording, and the PR description now uses ASP0039 with the 40 targeted/751 analyzer totals. The PR is ready for review, and VisitReturn is unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants