Skip to content

Commit 5c80441

Browse files
committed
Revert "Integrate LQRS004 ternary null check transformation into LQRS002/LQRS003 (#171)"
This reverts commit f551f00. (cherry picked from commit a0d1a02)
1 parent 41cdb70 commit 5c80441

File tree

8 files changed

+68
-584
lines changed

8 files changed

+68
-584
lines changed

docs/analyzers/LQRS002.md

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,6 @@ Detects `System.Linq` `Select` calls performed on `IQueryable<T>` whose selector
2222
- Convert `Select(...)``SelectExpr(...)` preserving the anonymous projection.
2323
- Convert `Select(...)``SelectExpr<TSource, TDto>(...)` with a generated DTO type when that is preferable.
2424

25-
### Automatic Ternary Null Check Simplification
26-
As of version 0.5.0, the code fix also **automatically simplifies ternary null check patterns** (previously handled by LQRS004). When converting `Select` to `SelectExpr`, patterns like:
27-
28-
```csharp
29-
prop = x.A != null ? new { x.A.B } : null
30-
```
31-
32-
are automatically converted to:
33-
34-
```csharp
35-
prop = new { B = x.A?.B }
36-
```
37-
38-
This provides more concise code using null-conditional operators. See [LQRS004](LQRS004.md) for details on this transformation.
39-
4025
## Example
4126
Before:
4227
```csharp
@@ -67,20 +52,3 @@ var result = query.SelectExpr<Product, ResultDto_XXXXXXXX>(x => new
6752
x.Price
6853
});
6954
```
70-
71-
### With Ternary Null Check Simplification
72-
Before:
73-
```csharp
74-
var result = query.Select(x => new
75-
{
76-
ChildData = x.Child != null ? new { x.Child.Name } : null
77-
});
78-
```
79-
80-
After (with automatic simplification):
81-
```csharp
82-
var result = query.SelectExpr(x => new
83-
{
84-
ChildData = new { Name = x.Child?.Name }
85-
});
86-
```

docs/analyzers/LQRS003.md

Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,36 +20,21 @@ Detects `System.Linq` `Select` invocations on `IQueryable<T>` whose selector cre
2020
## Code Fixes
2121
`SelectToSelectExprNamedCodeFixProvider` registers three distinct fixes (titles shown are from the provider):
2222

23-
- **Convert to SelectExpr<T, TDto>**
24-
- Converts the `Select` method to a generic `SelectExpr<TSource, TDto>` and **converts the named object creation into an anonymous object**. This variant recursively converts nested object creations as well (deep conversion). It also runs the ternary-null simplifier on the generated anonymous structures, converting patterns like `x.A != null ? x.A.B : null` to `x.A?.B`.
23+
- **Convert to SelectExpr<T, TDto> (convert all to anonymous)**
24+
- Converts the `Select` method to a generic `SelectExpr<TSource, TDto>` and **converts the named object creation into an anonymous object**. This variant recursively converts nested object creations as well (deep conversion). It also runs the ternary-null simplifier on the generated anonymous structures.
2525

26-
- **Convert to SelectExpr<T, TDto> (strict)**
27-
- Similar to the above, but **does NOT apply ternary null check simplification**. This preserves the original ternary patterns (e.g., `x.A != null ? x.A.B : null` remains unchanged). Useful when you want to maintain the exact nullability structure of the original code. You can apply [LQRS004](LQRS004.md) manually afterward if needed.
26+
- **Convert to SelectExpr<T, TDto> (convert root only to anonymous)**
27+
- Similar to the above, but converts only the reported (root) object creation to an anonymous object; nested object creations are left as-is. Produces a generic `SelectExpr<TSource, TDto>` and applies the ternary-null simplifier to the root anonymous creation.
2828

2929
- **Convert to SelectExpr (use predefined classes)**
30-
- Replaces the method name `Select` with `SelectExpr` (no generic type arguments) and preserves the existing named DTO type in the selector. This variant **does NOT apply ternary null check simplification**, preserving the original ternary patterns. You can apply [LQRS004](LQRS004.md) manually afterward if needed.
30+
- Replaces the method name `Select` with `SelectExpr` (no generic type arguments) and preserves the existing named DTO type in the selector. This variant also simplifies ternary-null checks inside the selector lambda but does not convert named object creation into anonymous objects.
3131

32-
### Automatic Ternary Null Check Simplification
33-
The first code fix option **automatically simplifies ternary null check patterns**. When converting `Select` to `SelectExpr`, patterns like:
34-
35-
```csharp
36-
prop = x.A != null ? x.A.B : null
37-
```
38-
39-
are automatically converted to:
40-
41-
```csharp
42-
prop = x.A?.B
43-
```
44-
45-
This provides more concise code using null-conditional operators. The second option (strict) and third option (predefined) intentionally preserve the original ternary patterns. You can use [LQRS004](LQRS004.md) to manually apply the transformation afterward.
46-
47-
These three options map directly to the code-fix implementations: `ConvertToSelectExprExplicitDtoAsync`, `ConvertToSelectExprExplicitDtoStructAsync`, and `ConvertToSelectExprPredefinedDtoAsync`.
32+
These three options map directly to the code-fix implementations: `ConvertToSelectExprExplicitDtoAllAsync`, `ConvertToSelectExprExplicitDtoRootOnlyAsync`, and `ConvertToSelectExprPredefinedDtoAsync`.
4833

4934
## Examples
5035
The examples below show the concrete transformations performed by each fix.
5136

52-
1) Convert to `SelectExpr<T, TDto>` (with ternary simplification)
37+
1) Convert to `SelectExpr<T, TDto>` (all)
5338

5439
Before:
5540
```csharp
@@ -59,30 +44,29 @@ var result = query
5944
{
6045
Id = x.Id,
6146
Name = x.Name,
62-
DetailDesc = x.Detail != null ? x.Detail.Desc : null
47+
Details = new ProductDetailDto { Desc = x.Detail.Desc }
6348
})
64-
.ToList();
49+
.Select(p => new WrapperDto { Item = p });
6550
```
6651

67-
After:
52+
After (all fix):
6853
```csharp
6954
var result = query
7055
.SelectMany(g => g.Items)
7156
.SelectExpr<Product, ResultDto_HASH>(x => new // root converted to anonymous
7257
{
7358
Id = x.Id,
7459
Name = x.Name,
75-
DetailDesc = x.Detail?.Desc // ternary simplified to null-conditional
60+
Details = new { Desc = x.Detail.Desc } // nested also converted to anonymous
7661
})
77-
.ToList();
62+
.SelectExpr(p => new WrapperDto { Item = p });
7863
```
7964

8065
Notes:
81-
- The named `ProductDto` initializer is converted to an anonymous initializer.
82-
- Ternary null check patterns are simplified to null-conditional operators.
83-
- A DTO name (e.g. `ResultDto_HASH`) is generated for the generic type argument.
66+
- Both the root `ProductDto` and the nested `ProductDetailDto` are converted to anonymous initializers (deep conversion).
67+
- The fixer inserts generic type arguments on the `SelectExpr` call using a generated DTO name.
8468

85-
2) Convert to `SelectExpr<T, TDto>` (struct - no ternary simplification)
69+
2) Convert to `SelectExpr<T, TDto>` (root-only)
8670

8771
Before:
8872
```csharp
@@ -92,28 +76,27 @@ var result = query
9276
{
9377
Id = x.Id,
9478
Name = x.Name,
95-
DetailDesc = x.Detail != null ? x.Detail.Desc : null
79+
Details = new ProductDetailDto { Desc = x.Detail.Desc }
9680
})
9781
.ToList();
9882
```
9983

100-
After (struct fix):
84+
After (root-only fix):
10185
```csharp
10286
var result = query
10387
.Where(p => p.IsActive)
10488
.SelectExpr<Product, ResultDto_HASH>(x => new // root converted to anonymous
10589
{
10690
Id = x.Id,
10791
Name = x.Name,
108-
DetailDesc = x.Detail != null ? x.Detail.Desc : null // ternary preserved
92+
Details = new ProductDetailDto { Desc = x.Detail.Desc } // nested remains named
10993
})
11094
.ToList();
11195
```
11296

11397
Notes:
114-
- The named `ProductDto` initializer is converted to an anonymous initializer.
115-
- Ternary null check patterns are **preserved** - no simplification is applied.
116-
- Use this option when you need to maintain the exact nullability structure.
98+
- The named `ProductDto` initializer is converted to an anonymous initializer at the root only.
99+
- A DTO name (e.g. `ResultDto_HASH`) is generated for the generic type argument; the code-fix inserts type arguments but replaces the selector body with an anonymous object.
117100

118101
3) Convert to `SelectExpr` (predefined)
119102

@@ -124,7 +107,7 @@ var result = query.Select(x => new ProductDto
124107
{
125108
Id = x.Id,
126109
Name = x.Name,
127-
DetailDesc = x.Detail != null ? x.Detail.Desc : null
110+
Details = new ProductDetailDto { Desc = x.Detail.Desc }
128111
});
129112
```
130113

@@ -134,11 +117,10 @@ var result = query.SelectExpr(x => new ProductDto // named DTO preserved
134117
{
135118
Id = x.Id,
136119
Name = x.Name,
137-
DetailDesc = x.Detail != null ? x.Detail.Desc : null // ternary preserved
120+
Details = new ProductDetailDto { Desc = x.Detail.Desc }
138121
});
139122
```
140123

141124
Notes:
142125
- This variant preserves the named DTO (`ProductDto`) and only changes the method to `SelectExpr`.
143-
- Ternary null check patterns are **preserved** - no simplification is applied.
144-
- You can apply [LQRS004](LQRS004.md) manually afterward if you want the simplified form.
126+
- Ternary-null simplifications inside the lambda are still applied where the simplifier can transform patterns safely.

docs/analyzers/LQRS004.md

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,21 @@
55
**Default:** Enabled
66

77
## Description
8-
Detects conditional (ternary) expressions where one branch returns `null` (or a nullable null-cast) and the other branch returns an object creation (anonymous or named), and the condition contains a null-check expression. Such patterns are often simplifiable using null-conditional (`?.`) chains or other null-safe idioms.
8+
Detects conditional (ternary) expressions where one branch returns `null` (or a nullable null-cast) and the other branch returns an object creation (anonymous or named), and the condition contains a null-check expression. Such patterns are often simplifyable using null-conditional (`?.`) chains or other null-safe idioms.
99

1010
## When It Triggers
1111
- The node is a conditional expression (`condition ? whenTrue : whenFalse`).
1212
- The condition contains a null check (e.g. `x != null`, `x == null`) or a logical-and chain that includes a null check.
1313
- One branch is `null` (or `(Type?)null`) and the other branch constructs an object (`new ...` or anonymous `new { ... }`).
14-
- The conditional is inside a `SelectExpr` call.
1514

1615
## When It Doesn't Trigger
1716
- Neither branch is an object creation.
1817
- The condition doesn't include a null-check.
19-
- The conditional is outside a `SelectExpr` call.
2018

2119
## Suggested Transformation
2220
The analyzer reports an informational diagnostic and suggests a null-propagation style replacement when the pattern is a simple null check that guards an object creation. A common safe transformation is to move the nullable operator into the member access so that inner member accesses use the null-conditional operator.
2321

24-
### Relationship with LQRS002/LQRS003
25-
26-
Some `Select``SelectExpr` conversion options automatically apply this transformation:
27-
- **LQRS002** (anonymous type): Always applies ternary simplification
28-
- **LQRS003 "Explicit"**: Applies ternary simplification
29-
30-
Others preserve the original ternary patterns:
31-
- **LQRS003 "Explicit (strict)"**: Does NOT apply ternary simplification
32-
- **LQRS003 "Predefined"**: Does NOT apply ternary simplification
33-
34-
For the conversions that preserve ternary patterns, you can use LQRS004 to manually apply the transformation afterward if desired.
22+
Because this transformation changes the position of nullability (e.g. from the selector producing `null` to the individual property access becoming nullable), it is handled separately from the `Select``SelectExpr` conversions (`LQRS002`/`LQRS003`). The `SelectExpr` conversions intentionally do not perform this rewrite automatically because moving the nullable operator may change semantics in subtle ways.
3523

3624
### Replacement example
3725
Given a conditional like:
@@ -43,4 +31,4 @@ the analyzer suggests replacing the object-creating ternary with a projection th
4331
new { B = x.A?.B }
4432
```
4533

46-
This effectively collapses the ternary and moves the `?.` into the member access, producing an object whose properties are null when the source is null instead of returning `null` for the whole object. Because the resulting nullability shape differs from the original expression, this transformation is offered as an informational suggestion.
34+
This effectively collapses the ternary and moves the `?.` into the member access, producing an object whose properties are null when the source is null instead of returning `null` for the whole object. Because the resulting nullability shape differs from the original expression, the analyzer treats this as a separate informational suggestion rather than part of `Select``SelectExpr` automated conversions.

src/Linqraft.Analyzer/SelectToSelectExprNamedCodeFixProvider.cs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,20 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
5050
// Register three code fixes
5151
context.RegisterCodeFix(
5252
CodeAction.Create(
53-
title: "Convert to SelectExpr<T, TDto>",
53+
title: "Convert to SelectExpr<T, TDto> (convert all to anonymous)",
5454
createChangedDocument: c =>
55-
ConvertToSelectExprExplicitDtoAsync(context.Document, invocation, c),
56-
equivalenceKey: "ConvertToSelectExprExplicitDto"
55+
ConvertToSelectExprExplicitDtoAllAsync(context.Document, invocation, c),
56+
equivalenceKey: "ConvertToSelectExprExplicitDtoAll"
5757
),
5858
diagnostic
5959
);
6060

6161
context.RegisterCodeFix(
6262
CodeAction.Create(
63-
title: "Convert to SelectExpr<T, TDto> (strict)",
63+
title: "Convert to SelectExpr<T, TDto> (convert root only to anonymous)",
6464
createChangedDocument: c =>
65-
ConvertToSelectExprExplicitDtoStructAsync(context.Document, invocation, c),
66-
equivalenceKey: "ConvertToSelectExprExplicitDtoStruct"
65+
ConvertToSelectExprExplicitDtoRootOnlyAsync(context.Document, invocation, c),
66+
equivalenceKey: "ConvertToSelectExprExplicitDtoRootOnly"
6767
),
6868
diagnostic
6969
);
@@ -79,7 +79,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
7979
);
8080
}
8181

82-
private static async Task<Document> ConvertToSelectExprExplicitDtoAsync(
82+
private static async Task<Document> ConvertToSelectExprExplicitDtoAllAsync(
8383
Document document,
8484
InvocationExpressionSyntax invocation,
8585
CancellationToken cancellationToken
@@ -164,11 +164,7 @@ CancellationToken cancellationToken
164164
.ConfigureAwait(false);
165165
}
166166

167-
/// <summary>
168-
/// Converts to SelectExpr with explicit DTO but WITHOUT ternary null check simplification.
169-
/// This maintains the traditional struct-like behavior where ternary patterns are preserved.
170-
/// </summary>
171-
private static async Task<Document> ConvertToSelectExprExplicitDtoStructAsync(
167+
private static async Task<Document> ConvertToSelectExprExplicitDtoRootOnlyAsync(
172168
Document document,
173169
InvocationExpressionSyntax invocation,
174170
CancellationToken cancellationToken
@@ -209,9 +205,12 @@ CancellationToken cancellationToken
209205
if (newExpression == null)
210206
return document;
211207

212-
// Convert the named object creation to anonymous type (including nested)
213-
// NOTE: No ternary simplification is applied here - this preserves the original structure
214-
var anonymousCreation = ConvertToAnonymousTypeRecursive(objectCreation);
208+
// Convert the named object creation to anonymous type (root only)
209+
var anonymousCreation = ConvertToAnonymousType(objectCreation, semanticModel);
210+
211+
// Simplify ternary null checks in the anonymous creation
212+
anonymousCreation = (AnonymousObjectCreationExpressionSyntax)
213+
TernaryNullCheckSimplifier.SimplifyTernaryNullChecks(anonymousCreation);
215214

216215
// Replace both nodes in one operation using a dictionary
217216
var replacements = new Dictionary<SyntaxNode, SyntaxNode>
@@ -274,9 +273,10 @@ CancellationToken cancellationToken
274273
if (newExpression == null)
275274
return document;
276275

277-
// For Predefined pattern, we do NOT simplify ternary null checks
278-
// Users can manually apply LQRS004 afterward if they want the simplified form
279-
var newInvocation = invocation.WithExpression(newExpression);
276+
// Simplify ternary null checks in the lambda body
277+
var newInvocation = TernaryNullCheckSimplifier.SimplifyTernaryNullChecksInInvocation(
278+
invocation.WithExpression(newExpression)
279+
);
280280

281281
// Add capture parameter if needed
282282
if (variablesToCapture.Count > 0)

0 commit comments

Comments
 (0)