Skip to content

Commit f5b1f06

Browse files
Indent expressions when converting to object initializer (#76334)
2 parents 9593ad2 + 951873e commit f5b1f06

File tree

5 files changed

+376
-19
lines changed

5 files changed

+376
-19
lines changed

src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/CSharpUseObjectInitializerCodeFixProvider.cs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@
77
using System.Diagnostics.CodeAnalysis;
88
using Microsoft.CodeAnalysis.CodeFixes;
99
using Microsoft.CodeAnalysis.CSharp.Extensions;
10+
using Microsoft.CodeAnalysis.CSharp.Formatting;
11+
using Microsoft.CodeAnalysis.CSharp.LanguageService;
1012
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
using Microsoft.CodeAnalysis.Formatting;
14+
using Microsoft.CodeAnalysis.LanguageService;
1115
using Microsoft.CodeAnalysis.PooledObjects;
1216
using Microsoft.CodeAnalysis.UseObjectInitializer;
1317

1418
namespace Microsoft.CodeAnalysis.CSharp.UseObjectInitializer;
1519

1620
using static CSharpSyntaxTokens;
21+
using static SyntaxFactory;
1722
using ObjectInitializerMatch = Match<ExpressionSyntax, StatementSyntax, MemberAccessExpressionSyntax, ExpressionStatementSyntax>;
1823

1924
[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseObjectInitializer), Shared]
@@ -34,25 +39,37 @@ internal sealed class CSharpUseObjectInitializerCodeFixProvider() :
3439
protected override CSharpUseNamedMemberInitializerAnalyzer GetAnalyzer()
3540
=> CSharpUseNamedMemberInitializerAnalyzer.Allocate();
3641

42+
protected override ISyntaxFormatting SyntaxFormatting => CSharpSyntaxFormatting.Instance;
43+
44+
protected override ISyntaxKinds SyntaxKinds => CSharpSyntaxKinds.Instance;
45+
46+
protected override SyntaxTrivia Whitespace(string text)
47+
=> SyntaxFactory.Whitespace(text);
48+
3749
protected override StatementSyntax GetNewStatement(
38-
StatementSyntax statement, BaseObjectCreationExpressionSyntax objectCreation,
50+
StatementSyntax statement,
51+
BaseObjectCreationExpressionSyntax objectCreation,
52+
SyntaxFormattingOptions options,
3953
ImmutableArray<ObjectInitializerMatch> matches)
4054
{
4155
return statement.ReplaceNode(
4256
objectCreation,
43-
GetNewObjectCreation(objectCreation, matches));
57+
GetNewObjectCreation(objectCreation, options, matches));
4458
}
4559

46-
private static BaseObjectCreationExpressionSyntax GetNewObjectCreation(
60+
private BaseObjectCreationExpressionSyntax GetNewObjectCreation(
4761
BaseObjectCreationExpressionSyntax objectCreation,
62+
SyntaxFormattingOptions options,
4863
ImmutableArray<ObjectInitializerMatch> matches)
4964
{
5065
return UseInitializerHelpers.GetNewObjectCreation(
51-
objectCreation, CreateExpressions(objectCreation, matches));
66+
objectCreation,
67+
CreateExpressions(objectCreation, options, matches));
5268
}
5369

54-
private static SeparatedSyntaxList<ExpressionSyntax> CreateExpressions(
70+
private SeparatedSyntaxList<ExpressionSyntax> CreateExpressions(
5571
BaseObjectCreationExpressionSyntax objectCreation,
72+
SyntaxFormattingOptions options,
5673
ImmutableArray<ObjectInitializerMatch> matches)
5774
{
5875
using var _ = ArrayBuilder<SyntaxNodeOrToken>.GetInstance(out var nodesAndTokens);
@@ -69,8 +86,9 @@ private static SeparatedSyntaxList<ExpressionSyntax> CreateExpressions(
6986

7087
var newTrivia = i == 0 ? trivia.WithoutLeadingBlankLines() : trivia;
7188

72-
var newAssignment = assignment.WithLeft(
73-
match.MemberAccessExpression.Name.WithLeadingTrivia(newTrivia));
89+
var newAssignment = assignment
90+
.WithLeft(match.MemberAccessExpression.Name.WithLeadingTrivia(newTrivia))
91+
.WithRight(Indent(assignment.Right, options));
7492

7593
if (i < matches.Length - 1)
7694
{
@@ -85,6 +103,6 @@ private static SeparatedSyntaxList<ExpressionSyntax> CreateExpressions(
85103
}
86104
}
87105

88-
return SyntaxFactory.SeparatedList<ExpressionSyntax>(nodesAndTokens);
106+
return SeparatedList<ExpressionSyntax>(nodesAndTokens);
89107
}
90108
}

src/Analyzers/CSharp/Tests/UseObjectInitializer/UseObjectInitializerTests.cs

Lines changed: 184 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Diagnostics.CodeAnalysis;
56
using System.Threading.Tasks;
67
using Microsoft.CodeAnalysis.CSharp;
7-
using Microsoft.CodeAnalysis.CSharp.Shared.Extensions;
88
using Microsoft.CodeAnalysis.CSharp.UseObjectInitializer;
99
using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions;
1010
using Microsoft.CodeAnalysis.Test.Utilities;
@@ -20,7 +20,9 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.UseObjectInitializer;
2020
[Trait(Traits.Feature, Traits.Features.CodeActionsUseObjectInitializer)]
2121
public partial class UseObjectInitializerTests
2222
{
23-
private static async Task TestMissingInRegularAndScriptAsync(string testCode, LanguageVersion? languageVersion = null)
23+
private static async Task TestMissingInRegularAndScriptAsync(
24+
[StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string testCode,
25+
LanguageVersion? languageVersion = null)
2426
{
2527
var test = new VerifyCS.Test
2628
{
@@ -1410,4 +1412,184 @@ void M()
14101412
LanguageVersion = LanguageVersion.CSharp12,
14111413
}.RunAsync();
14121414
}
1415+
1416+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/46665")]
1417+
public async Task TestIndentationOfMultiLineExpressions1()
1418+
{
1419+
await new VerifyCS.Test
1420+
{
1421+
TestCode = """
1422+
class C
1423+
{
1424+
string S;
1425+
string T;
1426+
1427+
void M(int i)
1428+
{
1429+
var c = [|new|] C();
1430+
c.S = i
1431+
.ToString();
1432+
c.T = i.
1433+
ToString();
1434+
}
1435+
}
1436+
""",
1437+
FixedCode = """
1438+
class C
1439+
{
1440+
string S;
1441+
string T;
1442+
1443+
void M(int i)
1444+
{
1445+
var c = [|new|] C
1446+
{
1447+
S = i
1448+
.ToString(),
1449+
T = i.
1450+
ToString()
1451+
};
1452+
}
1453+
}
1454+
""",
1455+
LanguageVersion = LanguageVersion.CSharp12,
1456+
}.RunAsync();
1457+
}
1458+
1459+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/46665")]
1460+
public async Task TestIndentationOfMultiLineExpressions2()
1461+
{
1462+
await new VerifyCS.Test
1463+
{
1464+
TestCode = """
1465+
class C
1466+
{
1467+
string S;
1468+
string T;
1469+
1470+
void M(int i)
1471+
{
1472+
var c = [|new|] C();
1473+
c.S = i
1474+
.ToString()
1475+
.ToString();
1476+
c.T = i.
1477+
ToString().
1478+
ToString();
1479+
}
1480+
}
1481+
""",
1482+
FixedCode = """
1483+
class C
1484+
{
1485+
string S;
1486+
string T;
1487+
1488+
void M(int i)
1489+
{
1490+
var c = [|new|] C
1491+
{
1492+
S = i
1493+
.ToString()
1494+
.ToString(),
1495+
T = i.
1496+
ToString().
1497+
ToString()
1498+
};
1499+
}
1500+
}
1501+
""",
1502+
LanguageVersion = LanguageVersion.CSharp12,
1503+
}.RunAsync();
1504+
}
1505+
1506+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/46665")]
1507+
public async Task TestIndentationOfMultiLineExpressions3()
1508+
{
1509+
await new VerifyCS.Test
1510+
{
1511+
TestCode = """
1512+
class C
1513+
{
1514+
string S;
1515+
string T;
1516+
1517+
void M(int i)
1518+
{
1519+
var c = [|new|] C();
1520+
c.S =
1521+
i.ToString();
1522+
c.T =
1523+
i.ToString();
1524+
}
1525+
}
1526+
""",
1527+
FixedCode = """
1528+
class C
1529+
{
1530+
string S;
1531+
string T;
1532+
1533+
void M(int i)
1534+
{
1535+
var c = [|new|] C
1536+
{
1537+
S =
1538+
i.ToString(),
1539+
T =
1540+
i.ToString()
1541+
};
1542+
}
1543+
}
1544+
""",
1545+
LanguageVersion = LanguageVersion.CSharp12,
1546+
}.RunAsync();
1547+
}
1548+
1549+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/46665")]
1550+
public async Task TestIndentationOfMultiLineExpressions4()
1551+
{
1552+
await new VerifyCS.Test
1553+
{
1554+
TestCode = """
1555+
class C
1556+
{
1557+
string S;
1558+
string T;
1559+
1560+
void M(int i)
1561+
{
1562+
var c = [|new|] C();
1563+
c.S =
1564+
i.ToString()
1565+
.ToString();
1566+
c.T =
1567+
i.ToString()
1568+
.ToString();
1569+
}
1570+
}
1571+
""",
1572+
FixedCode = """
1573+
class C
1574+
{
1575+
string S;
1576+
string T;
1577+
1578+
void M(int i)
1579+
{
1580+
var c = [|new|] C
1581+
{
1582+
S =
1583+
i.ToString()
1584+
.ToString(),
1585+
T =
1586+
i.ToString()
1587+
.ToString()
1588+
};
1589+
}
1590+
}
1591+
""",
1592+
LanguageVersion = LanguageVersion.CSharp12,
1593+
}.RunAsync();
1594+
}
14131595
}

src/Analyzers/Core/CodeFixes/UseObjectInitializer/AbstractUseObjectInitializerCodeFixProvider.cs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,13 @@ protected override (string title, string equivalenceKey) GetTitleAndEquivalenceK
4949

5050
protected abstract TAnalyzer GetAnalyzer();
5151

52+
protected abstract ISyntaxKinds SyntaxKinds { get; }
53+
protected abstract ISyntaxFormatting SyntaxFormatting { get; }
54+
55+
protected abstract SyntaxTrivia Whitespace(string text);
56+
5257
protected abstract TStatementSyntax GetNewStatement(
53-
TStatementSyntax statement, TObjectCreationExpressionSyntax objectCreation,
58+
TStatementSyntax statement, TObjectCreationExpressionSyntax objectCreation, SyntaxFormattingOptions options,
5459
ImmutableArray<Match<TExpressionSyntax, TStatementSyntax, TMemberAccessExpressionSyntax, TAssignmentStatementSyntax>> matches);
5560

5661
public override ImmutableArray<string> FixableDiagnosticIds
@@ -76,11 +81,66 @@ protected override async Task FixAsync(
7681
var statement = objectCreation.FirstAncestorOrSelf<TStatementSyntax>();
7782
Contract.ThrowIfNull(statement);
7883

79-
var newStatement = GetNewStatement(statement, objectCreation, matches)
80-
.WithAdditionalAnnotations(Formatter.Annotation);
84+
var firstToken = objectCreation.GetFirstToken();
85+
var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(
86+
this.SyntaxFormatting, cancellationToken).ConfigureAwait(false);
87+
88+
var newStatement = GetNewStatement(statement, objectCreation, formattingOptions, matches).WithAdditionalAnnotations(Formatter.Annotation);
8189

8290
editor.ReplaceNode(statement, newStatement);
8391
foreach (var match in matches)
8492
editor.RemoveNode(match.Statement, SyntaxRemoveOptions.KeepUnbalancedDirectives);
8593
}
94+
95+
protected TExpressionSyntax Indent(TExpressionSyntax expression, SyntaxFormattingOptions options)
96+
{
97+
var endOfLineKind = this.SyntaxKinds.EndOfLineTrivia;
98+
var whitespaceTriviaKind = this.SyntaxKinds.WhitespaceTrivia;
99+
return expression.ReplaceTokens(
100+
expression.DescendantTokens(),
101+
(currentToken, _) =>
102+
{
103+
if (currentToken.LeadingTrivia is [.., var whitespace1] &&
104+
whitespace1.RawKind == whitespaceTriviaKind)
105+
{
106+
// This is a token on its own line. With whitespace at the start of the line.
107+
var leadingTrivia = currentToken.LeadingTrivia.Replace(
108+
whitespace1,
109+
IncreaseIndent(whitespace1, options));
110+
111+
currentToken = currentToken.WithLeadingTrivia(leadingTrivia);
112+
}
113+
114+
if (currentToken.TrailingTrivia is [.., var endOfLine, var whitespace2] &&
115+
endOfLine.RawKind == endOfLineKind &&
116+
whitespace2.RawKind == whitespaceTriviaKind)
117+
{
118+
// This is a VB line continuation case (`_`), with indentation before the next token
119+
var trailingTrivia = currentToken.TrailingTrivia.Replace(
120+
whitespace2,
121+
IncreaseIndent(whitespace2, options));
122+
123+
currentToken = currentToken.WithTrailingTrivia(trailingTrivia);
124+
}
125+
126+
return currentToken;
127+
});
128+
}
129+
130+
private SyntaxTrivia IncreaseIndent(SyntaxTrivia whitespaceTrivia, SyntaxFormattingOptions options)
131+
{
132+
// Convert the existing whitespace to determine which column it corresponds to in spaces.
133+
var existingWhitespace = whitespaceTrivia.ToString();
134+
var spaceCount = existingWhitespace.ConvertTabToSpace(
135+
options.TabSize,
136+
initialColumn: 0,
137+
endPosition: existingWhitespace.Length);
138+
139+
// Then add the desired indentation spaces to it.
140+
var desiredSpaceCount = spaceCount + options.IndentationSize;
141+
142+
// Now convert back to a string with the appropriate tab/space configuration.
143+
var desiredWhitespace = desiredSpaceCount.CreateIndentationString(options.UseTabs, options.TabSize);
144+
return Whitespace(desiredWhitespace);
145+
}
86146
}

0 commit comments

Comments
 (0)