Skip to content

Commit d35ff5c

Browse files
csharpfritzCopilot
andcommitted
Fix DisplayExpressionTransform to emit idiomatic @expr for simple expressions
Simple dotted identifiers (Item.ProductName) now emit @Item.ProductName Complex expressions (method calls, operators) still emit @(expr) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent aa8f70c commit d35ff5c

6 files changed

Lines changed: 100 additions & 13 deletions

File tree

.squad/agents/bishop/history.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,9 @@
132132
- Extended `ServerShim` with `Transfer`, `GetLastError`, and `ClearError`, and updated `ServerShimTransform` guidance to treat those APIs as supported compatibility shims instead of dead-end TODOs.
133133
- Added focused regression tests for the new transforms, source-file copying, and ServerShim behavior; updated CLI docs plus migration docs so HttpUtility guidance now points to inline `WebUtility` rewrites rather than relying on the legacy shim package.
134134
- Validation: `dotnet build src\BlazorWebFormsComponents\BlazorWebFormsComponents.csproj --nologo`, `dotnet test src\BlazorWebFormsComponents.Test --nologo`, and `dotnet test tests\BlazorWebFormsComponents.Cli.Tests --nologo`.
135+
136+
### 2026-05-06T21:33:09-04:00: DisplayExpressionTransform emits idiomatic Razor for simple expressions (Bishop)
137+
- DisplayExpressionTransform now classifies dotted identifier chains with a dedicated regex helper and emits @expr for simple display expressions while preserving @(expr) for method calls, operators, and other complex expressions.
138+
- Updated focused coverage in tests\\BlazorWebFormsComponents.Cli.Tests\\TransformUnit\\DisplayExpressionTransformTests.cs for simple and complex cases, plus pipeline assertions for both shapes.
139+
- Updated L1 expected fixtures TC06-Expressions.razor and TC30-DataDrivenPage.razor to reflect idiomatic Razor output in generated markup.
140+
- Validation: dotnet test tests\\BlazorWebFormsComponents.Cli.Tests --nologo --filter DisplayExpression (9 passing) and dotnet test tests\\BlazorWebFormsComponents.Cli.Tests --nologo (573 passing).
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Decision: DisplayExpressionTransform emits idiomatic Razor for simple expressions
2+
3+
**Date:** 2026-05-06
4+
**Author:** Bishop (Migration Tooling Dev)
5+
**Status:** Implemented
6+
7+
## Decision
8+
9+
Update `DisplayExpressionTransform` so simple dotted identifier display expressions emit bare Razor `@expr`, while complex expressions continue to emit `@(expr)`.
10+
11+
## Scope
12+
13+
- Simple expressions match identifier chains such as `Item.ProductName`, `Item.UnitPrice`, `Model.Title`, and `user.Email`.
14+
- Complex expressions keep parentheses, including method calls, operators, ternaries, indexers, and casts.
15+
- Broken generated `@(: expr)` output is normalized through the same formatter so simple cases become idiomatic Razor too.
16+
17+
## Rationale
18+
19+
The old transform emitted `@(expr)` for every display expression. That output compiled, but it produced noisy, non-idiomatic Razor for the most common migrated shape: property access on the current item or model.
20+
21+
Using bare `@expr` for simple member chains keeps generated Razor closer to what a Blazor developer would naturally write, without sacrificing correctness for complex expressions that still require grouping.
22+
23+
## Files Updated
24+
25+
- `src\BlazorWebFormsComponents.Cli\Transforms\Markup\DisplayExpressionTransform.cs`
26+
- `tests\BlazorWebFormsComponents.Cli.Tests\TransformUnit\DisplayExpressionTransformTests.cs`
27+
- `tests\BlazorWebFormsComponents.Cli.Tests\TestData\expected\TC06-Expressions.razor`
28+
- `tests\BlazorWebFormsComponents.Cli.Tests\TestData\expected\TC30-DataDrivenPage.razor`
29+
30+
## Validation
31+
32+
- `dotnet test tests\BlazorWebFormsComponents.Cli.Tests --nologo --filter "DisplayExpression"`
33+
- `dotnet test tests\BlazorWebFormsComponents.Cli.Tests --nologo`

src/BlazorWebFormsComponents.Cli/Transforms/Markup/DisplayExpressionTransform.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace BlazorWebFormsComponents.Cli.Transforms.Markup;
66
/// <summary>
77
/// Normalizes Web Forms display expressions before broader expression handling.
88
/// Converts HTML-encoded display blocks and broken generated Razor output into
9-
/// standard Razor <c>@(...)</c> expressions.
9+
/// idiomatic Razor expressions.
1010
/// </summary>
1111
public class DisplayExpressionTransform : IMarkupTransform
1212
{
@@ -22,14 +22,29 @@ public class DisplayExpressionTransform : IMarkupTransform
2222
@"@\(\s*:\s*(.*?)\s*\)",
2323
RegexOptions.Compiled | RegexOptions.Singleline);
2424

25+
private static readonly Regex SimpleExpressionRegex = new(
26+
@"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$",
27+
RegexOptions.Compiled);
28+
2529
public string Name => "DisplayExpression";
2630
public int Order => 490;
2731

2832
public string Apply(string content, FileMetadata metadata)
2933
{
30-
content = DisplayExpressionRegex.Replace(content, match => $"@({match.Groups[1].Value.Trim()})");
31-
content = ColonEqualsExpressionRegex.Replace(content, match => $"@({match.Groups[1].Value.Trim()})");
32-
content = BrokenRazorDisplayRegex.Replace(content, match => $"@({match.Groups[1].Value.Trim()})");
34+
content = DisplayExpressionRegex.Replace(content, match => FormatExpression(match.Groups[1].Value));
35+
content = ColonEqualsExpressionRegex.Replace(content, match => FormatExpression(match.Groups[1].Value));
36+
content = BrokenRazorDisplayRegex.Replace(content, match => FormatExpression(match.Groups[1].Value));
3337
return content;
3438
}
39+
40+
private static string FormatExpression(string expression)
41+
{
42+
var expr = expression.Trim();
43+
return IsSimpleExpression(expr) ? "@" + expr : "@(" + expr + ")";
44+
}
45+
46+
private static bool IsSimpleExpression(string expr)
47+
{
48+
return SimpleExpressionRegex.IsMatch(expr);
49+
}
3550
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@page "/TC06-Expressions"
22
<PageTitle>Test</PageTitle>
33
<span>@context.Name</span>
4-
<span>@(Item.Price)</span>
4+
<span>@Item.Price</span>
55
<span>@(DateTime.Now)</span>
66
<span>@(someVar)</span>

tests/BlazorWebFormsComponents.Cli.Tests/TestData/expected/TC30-DataDrivenPage.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<ItemTemplate>
2424
<div class="product-card">
2525
<h4>@context.ProductName</h4>
26-
<span class="price">@(Item.UnitPrice)</span>
26+
<span class="price">@Item.UnitPrice</span>
2727
</div>
2828
</ItemTemplate>
2929
</Repeater>

tests/BlazorWebFormsComponents.Cli.Tests/TransformUnit/DisplayExpressionTransformTests.cs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,43 @@ public void HasExpectedMetadata()
2323
}
2424

2525
[Fact]
26-
public void ConvertsEncodedDisplayExpression()
26+
public void ConvertsEncodedDisplayExpressionToIdiomaticRazorForSimpleExpression()
2727
{
2828
var result = _transform.Apply("<span><%#: Item.Name %></span>", TestMetadata);
2929

30-
Assert.Equal("<span>@(Item.Name)</span>", result);
30+
Assert.Equal("<span>@Item.Name</span>", result);
3131
}
3232

3333
[Fact]
34-
public void ConvertsColonEqualsDisplayExpression()
34+
public void ConvertsColonEqualsDisplayExpressionToIdiomaticRazorForSimpleExpression()
3535
{
3636
var result = _transform.Apply("<span><%=: someVar %></span>", TestMetadata);
3737

38-
Assert.Equal("<span>@(someVar)</span>", result);
38+
Assert.Equal("<span>@someVar</span>", result);
3939
}
4040

4141
[Fact]
42-
public void FixesBrokenGeneratedRazorDisplayExpression()
42+
public void KeepsParenthesesForMethodCallExpression()
43+
{
44+
var result = _transform.Apply("<span><%#: item.GetPrice() %></span>", TestMetadata);
45+
46+
Assert.Equal("<span>@(item.GetPrice())</span>", result);
47+
}
48+
49+
[Fact]
50+
public void KeepsParenthesesForOperatorExpression()
51+
{
52+
var result = _transform.Apply("<span><%#: Item.UnitPrice + tax %></span>", TestMetadata);
53+
54+
Assert.Equal("<span>@(Item.UnitPrice + tax)</span>", result);
55+
}
56+
57+
[Fact]
58+
public void FixesBrokenGeneratedRazorDisplayExpressionToIdiomaticRazorForSimpleExpression()
4359
{
4460
var result = _transform.Apply("<span>@(: expr)</span>", TestMetadata);
4561

46-
Assert.Equal("<span>@(expr)</span>", result);
62+
Assert.Equal("<span>@expr</span>", result);
4763
}
4864

4965
[Fact]
@@ -68,6 +84,23 @@ public void PipelinePreservesNormalizedDisplayExpression()
6884

6985
var result = pipeline.TransformMarkup("<ItemTemplate><%#: Item.Name %></ItemTemplate>", metadata);
7086

71-
Assert.Contains("@(Item.Name)", result);
87+
Assert.Contains("@Item.Name", result);
88+
}
89+
90+
[Fact]
91+
public void PipelinePreservesParenthesesForComplexDisplayExpression()
92+
{
93+
var pipeline = TestHelpers.CreateDefaultPipeline();
94+
var metadata = new FileMetadata
95+
{
96+
SourceFilePath = "Product.aspx",
97+
OutputFilePath = "Product.razor",
98+
FileType = FileType.Page,
99+
OriginalContent = string.Empty
100+
};
101+
102+
var result = pipeline.TransformMarkup("<ItemTemplate><%#: item.GetPrice() %></ItemTemplate>", metadata);
103+
104+
Assert.Contains("@(item.GetPrice())", result);
72105
}
73106
}

0 commit comments

Comments
 (0)