Skip to content

Improve XEXPR diagnostics for interpolated strings and lambda method groups#35093

Open
StephaneDelcroix wants to merge 1 commit intomainfrom
fix/xexpr-better-diagnostics
Open

Improve XEXPR diagnostics for interpolated strings and lambda method groups#35093
StephaneDelcroix wants to merge 1 commit intomainfrom
fix/xexpr-better-diagnostics

Conversation

@StephaneDelcroix
Copy link
Copy Markdown
Contributor

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description

Two XAML C# expression (XEXPR) scenarios currently produce C# compilation errors instead of source generator diagnostics, making errors harder to understand and fix. This PR catches both at source generation time with clear messages.

Issue 1: Interpolated string with nonexistent member

<Label Text="{$'Hello, {Name1}!'}" />

When Name1 doesn't exist on the page or ViewModel, the source gen previously emitted $"Hello, {__source.Name1}!" verbatim, resulting in a cryptic C# compilation error. Now MAUIX2009 is reported: "'Name1' not found on 'MyPage' or 'MyViewModel'."

Root cause: The existing MAUIX2009 check only ran for "simple identifiers" (IsSimpleIdentifier returns false for interpolated strings due to $, {, } chars). The fix extracts identifiers from interpolation holes using Roslyn and validates each against MemberResolver.

Issue 2: Lambda with method group instead of invocation

<Button Clicked="{(s, e) => this.OnCounterClicked}" />

Missing () caused the source gen to emit button.Clicked += (s, e) => this.OnCounterClicked; — invalid C# (method group can't be expression body of void-returning lambda). Now MAUIX2017 is reported: "Lambda body 'this.OnCounterClicked' is a method group reference, not a method invocation. Did you mean 'this.OnCounterClicked(s, e)'?"

Root cause: ConnectEvent() emitted lambda expressions with zero validation. The fix parses the lambda body with Roslyn to detect MemberAccessExpression without invocation.

Changes

File Change
CSharpExpressionHelpers.cs New ExtractInterpolatedStringIdentifiers() and DetectLambdaMethodGroupReference() helpers
SetPropertyHelpers.cs Interpolated string member validation in TryHandleExpressionBinding(); method group check in ConnectEvent()
Descriptors.cs New MAUIX2017 diagnostic descriptor
AnalyzerReleases.Unshipped.md Register MAUIX2017
CSharpExpressionDiagnosticsTests.cs 2 new tests (204/204 pass)

…groups

- Report MAUIX2009 when interpolated string holes reference nonexistent
  members (e.g., {$'Hello, {Name1}!'} where Name1 doesn't exist),
  instead of falling through to a C# compilation error.

- Add MAUIX2017 diagnostic when lambda event handler body is a method
  group reference instead of an invocation (e.g., {(s, e) => this.OnClicked}
  missing parentheses), with a suggestion to add them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 22, 2026 14:41
@github-actions
Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35093

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35093"

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 improves .NET MAUI XAML C# expression (XEXPR) source-generator diagnostics so that certain invalid expressions fail with MAUIX* diagnostics instead of falling through to cryptic C# compiler errors.

Changes:

  • Added Roslyn-based extraction of identifiers from interpolated string holes to report MAUIX2009 at source generation time.
  • Added a new MAUIX2017 diagnostic for event-handler lambdas whose body is a method group reference (missing invocation).
  • Added unit tests and registered the new diagnostic in analyzer release notes.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/Controls/tests/SourceGen.UnitTests/CSharpExpressionDiagnosticsTests.cs Adds coverage for interpolated-string missing member and lambda method-group diagnostics.
src/Controls/src/SourceGen/SetPropertyHelpers.cs Emits MAUIX2009 for missing members inside interpolated strings; emits MAUIX2017 for method-group lambda bodies in ConnectEvent.
src/Controls/src/SourceGen/Descriptors.cs Introduces MAUIX2017 descriptor definition.
src/Controls/src/SourceGen/CSharpExpressionHelpers.cs Adds helper methods to parse interpolated strings and detect method-group lambda bodies.
src/Controls/src/SourceGen/AnalyzerReleases.Unshipped.md Registers MAUIX2017 in the unshipped analyzer release list.

Comment on lines +718 to +732
// Validate identifiers inside interpolated string holes
if (expression.Code.StartsWith("$\"", StringComparison.Ordinal) || expression.Code.StartsWith("$@\"", StringComparison.Ordinal))
{
var interpolatedIds = CSharpExpressionHelpers.ExtractInterpolatedStringIdentifiers(expression.Code);
foreach (var id in interpolatedIds)
{
var idResolution = MemberResolver.Resolve(id, context.RootType, dataTypeSymbol, context.Compilation);
if (idResolution.Location == MemberLocation.Neither)
{
var idLocation = LocationCreate(context.ProjectItem.RelativePath!, (IXmlLineInfo)valueNode, expression.Code);
context.ReportDiagnostic(Diagnostic.Create(Descriptors.MemberNotFound, idLocation, id, context.RootType?.Name ?? "this", dataTypeSymbol.Name));
return true; // Handled (with error)
}
}
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

The interpolated-string identifier validation will incorrectly report MAUIX2009 for identifiers that are not members on the page/ViewModel but are still valid C# in interpolation holes (e.g., DateTime.Now, System.String, Math.PI). In those cases MemberResolver.Resolve(id, ...) returns Neither and this code turns otherwise-valid expressions into generator errors. Consider skipping identifiers that resolve to a type/namespace (via the Compilation) or performing semantic analysis on each interpolation hole so only actual member references against this/x:DataType are validated.

Copilot uses AI. Check for mistakes.
Comment on lines +719 to +721
if (expression.Code.StartsWith("$\"", StringComparison.Ordinal) || expression.Code.StartsWith("$@\"", StringComparison.Ordinal))
{
var interpolatedIds = CSharpExpressionHelpers.ExtractInterpolatedStringIdentifiers(expression.Code);
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

This interpolated-string check only triggers for expression.Code starting with $" or $@". Valid interpolated strings can also start with @$" (and may have leading whitespace), so those cases will skip identifier validation and still fall through to C# compilation errors. Consider using TrimStart() plus handling @$", or (more robust) detecting interpolated-string syntax via Roslyn rather than string prefix checks.

Suggested change
if (expression.Code.StartsWith("$\"", StringComparison.Ordinal) || expression.Code.StartsWith("$@\"", StringComparison.Ordinal))
{
var interpolatedIds = CSharpExpressionHelpers.ExtractInterpolatedStringIdentifiers(expression.Code);
var trimmedExpressionCode = expression.Code.TrimStart();
if (trimmedExpressionCode.StartsWith("$\"", StringComparison.Ordinal) ||
trimmedExpressionCode.StartsWith("$@\"", StringComparison.Ordinal) ||
trimmedExpressionCode.StartsWith("@$\"", StringComparison.Ordinal))
{
var interpolatedIds = CSharpExpressionHelpers.ExtractInterpolatedStringIdentifiers(trimmedExpressionCode);

Copilot uses AI. Check for mistakes.
Comment on lines +969 to +981
var parameters = string.Join(", ", lambda.ParameterList.Parameters.Select(p => p.Identifier.Text));
return (memberAccess.ToString(), parameters);
}

// Also check for bare identifier (e.g., (s, e) => OnClicked)
if (body is IdentifierNameSyntax identifier)
{
var parameters = string.Join(", ", lambda.ParameterList.Parameters.Select(p => p.Identifier.Text));
return (identifier.Identifier.Text, parameters);
}

return null;
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

DetectLambdaMethodGroupReference builds the suggested invocation argument list from the lambda parameter identifiers. If the lambda uses discard parameters (e.g. (_, _) => this.OnClicked) the diagnostic will suggest this.OnClicked(_, _), which does not compile because discards can’t be referenced. Consider omitting parameter names when any parameter is _, or generating safe placeholders (e.g. arg0, arg1) for the message.

Suggested change
var parameters = string.Join(", ", lambda.ParameterList.Parameters.Select(p => p.Identifier.Text));
return (memberAccess.ToString(), parameters);
}
// Also check for bare identifier (e.g., (s, e) => OnClicked)
if (body is IdentifierNameSyntax identifier)
{
var parameters = string.Join(", ", lambda.ParameterList.Parameters.Select(p => p.Identifier.Text));
return (identifier.Identifier.Text, parameters);
}
return null;
}
var parameters = GetLambdaInvocationArgumentList(lambda.ParameterList.Parameters);
return (memberAccess.ToString(), parameters);
}
// Also check for bare identifier (e.g., (s, e) => OnClicked)
if (body is IdentifierNameSyntax identifier)
{
var parameters = GetLambdaInvocationArgumentList(lambda.ParameterList.Parameters);
return (identifier.Identifier.Text, parameters);
}
return null;
}
static string GetLambdaInvocationArgumentList(SeparatedSyntaxList<ParameterSyntax> parameters)
{
var parameterNames = parameters.Select(p => p.Identifier.Text).ToArray();
if (parameterNames.Any(name => name == "_"))
return string.Join(", ", parameterNames.Select((_, index) => $"arg{index}"));
return string.Join(", ", parameterNames);
}

Copilot uses AI. Check for mistakes.
Assert.Contains(result.Diagnostics, d => d.Id == "MAUIX2009");
}

[Fact]
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

The new interpolated-string validation adds a new failure mode (false positives on static types/namespaces like DateTime.Now / System.String). Consider adding a unit test that asserts no MAUIX2009 is reported for an interpolated string that uses a static type in an interpolation hole, to prevent regressions once the validation logic is adjusted.

Suggested change
[Fact]
[Fact]
public void InterpolatedStaticTypeReference_DoesNotReportMAUIX2009()
{
var xaml =
"""
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:Class="TestApp.InterpolatedStaticTypePage">
<Label Text="{Binding $`Now: {sys:DateTime.Now}`}" />
</ContentPage>
""";
var codeBehind =
"""
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
namespace TestApp;
[XamlProcessing(XamlInflator.SourceGen)]
public partial class InterpolatedStaticTypePage : ContentPage
{
public InterpolatedStaticTypePage() => InitializeComponent();
}
""";
var (result, _) = RunGenerator(xaml, codeBehind, assertNoCompilationErrors: false);
Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIX2009");
}
[Fact]

Copilot uses AI. Check for mistakes.
@StephaneDelcroix
Copy link
Copy Markdown
Contributor Author

@noiseonwires

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Apr 24, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 24, 2026
@MauiBot MauiBot added the s/agent-fix-win AI found a better alternative fix than the PR label Apr 24, 2026
Copy link
Copy Markdown
Contributor

@kubaflo kubaflo left a comment

Choose a reason for hiding this comment

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

@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented May 3, 2026

⚠️ Merge Conflict Detected — This PR has merge conflicts with its target branch. Please rebase onto the target branch and resolve the conflicts.

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

Labels

s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants