Skip to content

Commit 2b8a9f7

Browse files
author
Andrew Hall
authored
Merge pull request #40070 from CyrusNajmabadi/simplifyInterpolation
Add feature to simplify interpolation expressions when possible
2 parents e2f4946 + 8971af1 commit 2b8a9f7

File tree

37 files changed

+1711
-20
lines changed

37 files changed

+1711
-20
lines changed

src/EditorFeatures/CSharpTest/SimplifyInterpolation/SimplifyInterpolationTests.cs

Lines changed: 566 additions & 0 deletions
Large diffs are not rendered by default.

src/EditorFeatures/Test/Diagnostics/IDEDiagnosticIDConfigurationTests.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,9 @@ public void CSharp_VerifyIDEDiagnosticSeveritiesAreConfigurable()
353353
# IDE0070
354354
dotnet_diagnostic.IDE0070.severity = %value%
355355
356+
# IDE0071
357+
dotnet_style_prefer_simplified_interpolation = true:suggestion
358+
356359
# IDE0072
357360
dotnet_diagnostic.IDE0072.severity = %value%
358361
@@ -487,6 +490,9 @@ public void VisualBasic_VerifyIDEDiagnosticSeveritiesAreConfigurable()
487490
# IDE0070
488491
dotnet_diagnostic.IDE0070.severity = %value%
489492
493+
# IDE0071
494+
dotnet_style_prefer_simplified_interpolation = true:suggestion
495+
490496
# IDE1006
491497
dotnet_diagnostic.IDE1006.severity = %value%
492498
@@ -835,6 +841,9 @@ No editorconfig based code style option
835841
# IDE0070
836842
No editorconfig based code style option
837843
844+
# IDE0071, PreferSimplifiedInterpolation
845+
dotnet_style_prefer_simplified_interpolation = true:suggestion
846+
838847
# IDE0072
839848
No editorconfig based code style option
840849
@@ -1002,6 +1011,9 @@ No editorconfig based code style option
10021011
# IDE0070
10031012
No editorconfig based code style option
10041013
1014+
# IDE0071, PreferSimplifiedInterpolation
1015+
dotnet_style_prefer_simplified_interpolation = true:suggestion
1016+
10051017
# IDE1006
10061018
No editorconfig based code style option
10071019

src/EditorFeatures/TestUtilities/CodeActions/AbstractCodeActionOrUserDiagnosticTest.cs

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@
44
using System.Collections.Generic;
55
using System.Collections.Immutable;
66
using System.Linq;
7+
using System.Text;
78
using System.Threading;
89
using System.Threading.Tasks;
910
using Microsoft.CodeAnalysis;
1011
using Microsoft.CodeAnalysis.CodeActions;
11-
using Microsoft.CodeAnalysis.CodeFixes.Suppression;
1212
using Microsoft.CodeAnalysis.CodeStyle;
1313
using Microsoft.CodeAnalysis.Editor.Implementation.Preview;
1414
using Microsoft.CodeAnalysis.Editor.UnitTests.Extensions;
1515
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
1616
using Microsoft.CodeAnalysis.Options;
17-
using Microsoft.CodeAnalysis.Remote;
1817
using Microsoft.CodeAnalysis.Shared.Utilities;
1918
using Microsoft.CodeAnalysis.Test.Utilities;
2019
using Microsoft.CodeAnalysis.Text;
@@ -37,6 +36,7 @@ public struct TestParameters
3736
internal readonly int index;
3837
internal readonly CodeActionPriority? priority;
3938
internal readonly bool retainNonFixableDiagnostics;
39+
internal readonly bool includeDiagnosticsOutsideSelection;
4040
internal readonly string title;
4141

4242
internal TestParameters(
@@ -47,6 +47,7 @@ internal TestParameters(
4747
int index = 0,
4848
CodeActionPriority? priority = null,
4949
bool retainNonFixableDiagnostics = false,
50+
bool includeDiagnosticsOutsideSelection = false,
5051
string title = null)
5152
{
5253
this.parseOptions = parseOptions;
@@ -56,23 +57,27 @@ internal TestParameters(
5657
this.index = index;
5758
this.priority = priority;
5859
this.retainNonFixableDiagnostics = retainNonFixableDiagnostics;
60+
this.includeDiagnosticsOutsideSelection = includeDiagnosticsOutsideSelection;
5961
this.title = title;
6062
}
6163

6264
public TestParameters WithParseOptions(ParseOptions parseOptions)
63-
=> new TestParameters(parseOptions, compilationOptions, options, fixProviderData, index, priority, title: title);
65+
=> new TestParameters(parseOptions, compilationOptions, options, fixProviderData, index, priority, retainNonFixableDiagnostics, includeDiagnosticsOutsideSelection, title);
6466

6567
public TestParameters WithOptions(IDictionary<OptionKey, object> options)
66-
=> new TestParameters(parseOptions, compilationOptions, options, fixProviderData, index, priority, title: title);
68+
=> new TestParameters(parseOptions, compilationOptions, options, fixProviderData, index, priority, retainNonFixableDiagnostics, includeDiagnosticsOutsideSelection, title);
6769

6870
public TestParameters WithFixProviderData(object fixProviderData)
69-
=> new TestParameters(parseOptions, compilationOptions, options, fixProviderData, index, priority, title: title);
71+
=> new TestParameters(parseOptions, compilationOptions, options, fixProviderData, index, priority, retainNonFixableDiagnostics, includeDiagnosticsOutsideSelection, title);
7072

7173
public TestParameters WithIndex(int index)
72-
=> new TestParameters(parseOptions, compilationOptions, options, fixProviderData, index, priority, title: title);
74+
=> new TestParameters(parseOptions, compilationOptions, options, fixProviderData, index, priority, retainNonFixableDiagnostics, includeDiagnosticsOutsideSelection, title);
7375

7476
public TestParameters WithRetainNonFixableDiagnostics(bool retainNonFixableDiagnostics)
75-
=> new TestParameters(parseOptions, compilationOptions, options, fixProviderData, index, priority, title: title, retainNonFixableDiagnostics: retainNonFixableDiagnostics);
77+
=> new TestParameters(parseOptions, compilationOptions, options, fixProviderData, index, priority, retainNonFixableDiagnostics, includeDiagnosticsOutsideSelection, title);
78+
79+
public TestParameters WithIncludeDiagnosticsOutsideSelection(bool includeDiagnosticsOutsideSelection)
80+
=> new TestParameters(parseOptions, compilationOptions, options, fixProviderData, index, priority, retainNonFixableDiagnostics, includeDiagnosticsOutsideSelection, title);
7681
}
7782

7883
protected abstract string GetLanguage();
@@ -373,17 +378,35 @@ private async Task TestAsync(
373378
CodeActionPriority? priority,
374379
TestParameters parameters)
375380
{
381+
MarkupTestFile.GetSpans(
382+
initialMarkup.NormalizeLineEndings(),
383+
out var initialMarkupWithoutSpans, out IDictionary<string, ImmutableArray<TextSpan>> initialSpanMap);
384+
385+
const string UnnecessaryMarkupKey = "Unnecessary";
386+
var unnecessarySpans = initialSpanMap.GetOrAdd(UnnecessaryMarkupKey, _ => ImmutableArray<TextSpan>.Empty);
387+
376388
MarkupTestFile.GetSpans(
377389
expectedMarkup.NormalizeLineEndings(),
378-
out var expected, out IDictionary<string, ImmutableArray<TextSpan>> spanMap);
390+
out var expected, out IDictionary<string, ImmutableArray<TextSpan>> expectedSpanMap);
379391

380-
var conflictSpans = spanMap.GetOrAdd("Conflict", _ => ImmutableArray<TextSpan>.Empty);
381-
var renameSpans = spanMap.GetOrAdd("Rename", _ => ImmutableArray<TextSpan>.Empty);
382-
var warningSpans = spanMap.GetOrAdd("Warning", _ => ImmutableArray<TextSpan>.Empty);
383-
var navigationSpans = spanMap.GetOrAdd("Navigation", _ => ImmutableArray<TextSpan>.Empty);
392+
var conflictSpans = expectedSpanMap.GetOrAdd("Conflict", _ => ImmutableArray<TextSpan>.Empty);
393+
var renameSpans = expectedSpanMap.GetOrAdd("Rename", _ => ImmutableArray<TextSpan>.Empty);
394+
var warningSpans = expectedSpanMap.GetOrAdd("Warning", _ => ImmutableArray<TextSpan>.Empty);
395+
var navigationSpans = expectedSpanMap.GetOrAdd("Navigation", _ => ImmutableArray<TextSpan>.Empty);
384396

385397
using (var workspace = CreateWorkspaceFromOptions(initialMarkup, parameters))
386398
{
399+
// Ideally this check would always run, but there are several hundred tests that would need to be
400+
// updated with {|Unnecessary:|} spans.
401+
if (unnecessarySpans.Any())
402+
{
403+
var allDiagnostics = await GetDiagnosticsWorkerAsync(workspace, parameters
404+
.WithRetainNonFixableDiagnostics(true)
405+
.WithIncludeDiagnosticsOutsideSelection(true));
406+
407+
TestDiagnosticTags(allDiagnostics, unnecessarySpans, WellKnownDiagnosticTags.Unnecessary, UnnecessaryMarkupKey, initialMarkupWithoutSpans);
408+
}
409+
387410
var (_, action) = await GetCodeActionsAsync(workspace, parameters);
388411
await TestActionAsync(
389412
workspace, expected, action,
@@ -392,6 +415,85 @@ await TestActionAsync(
392415
}
393416
}
394417

418+
private static void TestDiagnosticTags(
419+
ImmutableArray<Diagnostic> diagnostics,
420+
ImmutableArray<TextSpan> expectedSpans,
421+
string diagnosticTag,
422+
string markupKey,
423+
string initialMarkupWithoutSpans)
424+
{
425+
var diagnosticsWithTag = diagnostics
426+
.Where(d => d.Descriptor.CustomTags.Contains(diagnosticTag))
427+
.OrderBy(s => s.Location.SourceSpan.Start)
428+
.ToImmutableArray();
429+
430+
if (expectedSpans.Length != diagnosticsWithTag.Length)
431+
{
432+
AssertEx.Fail(BuildFailureMessage(expectedSpans, diagnosticTag, markupKey, initialMarkupWithoutSpans, diagnosticsWithTag));
433+
}
434+
435+
for (var i = 0; i < expectedSpans.Length; i++)
436+
{
437+
var actual = diagnosticsWithTag[i].Location.SourceSpan;
438+
var expected = expectedSpans[i];
439+
Assert.Equal(expected, actual);
440+
}
441+
}
442+
443+
private static string BuildFailureMessage(
444+
ImmutableArray<TextSpan> expectedSpans,
445+
string diagnosticTag,
446+
string markupKey,
447+
string initialMarkupWithoutSpans,
448+
ImmutableArray<Diagnostic> diagnosticsWithTag)
449+
{
450+
var message = $"Expected {expectedSpans.Length} diagnostic spans with custom tag '{diagnosticTag}', but there were {diagnosticsWithTag.Length}.";
451+
452+
if (expectedSpans.Length == 0)
453+
{
454+
message += $" If a diagnostic span tagged '{diagnosticTag}' is expected, surround the span in the test markup with the following syntax: {{|Unnecessary:...}}";
455+
456+
var segments = new List<(int originalStringIndex, string segment)>();
457+
458+
foreach (var diagnostic in diagnosticsWithTag)
459+
{
460+
var documentOffset = initialMarkupWithoutSpans.IndexOf(diagnosticsWithTag.First().Location.SourceTree.ToString());
461+
if (documentOffset == -1) continue;
462+
463+
segments.Add((documentOffset + diagnostic.Location.SourceSpan.Start, "{|" + markupKey + ":"));
464+
segments.Add((documentOffset + diagnostic.Location.SourceSpan.End, "|}"));
465+
}
466+
467+
if (segments.Any())
468+
{
469+
message += Environment.NewLine
470+
+ "Example:" + Environment.NewLine
471+
+ Environment.NewLine
472+
+ InsertSegments(initialMarkupWithoutSpans, segments);
473+
}
474+
}
475+
476+
return message;
477+
}
478+
479+
private static string InsertSegments(string originalString, IEnumerable<(int originalStringIndex, string segment)> segments)
480+
{
481+
var builder = new StringBuilder();
482+
483+
var positionInOriginalString = 0;
484+
485+
foreach (var (originalStringIndex, segment) in segments.OrderBy(s => s.originalStringIndex))
486+
{
487+
builder.Append(originalString, positionInOriginalString, originalStringIndex - positionInOriginalString);
488+
builder.Append(segment);
489+
490+
positionInOriginalString = originalStringIndex;
491+
}
492+
493+
builder.Append(originalString, positionInOriginalString, originalString.Length - positionInOriginalString);
494+
return builder.ToString();
495+
}
496+
395497
internal async Task<Tuple<Solution, Solution>> TestActionAsync(
396498
TestWorkspace workspace, string expected,
397499
CodeAction action,
@@ -659,7 +761,7 @@ internal static IDictionary<OptionKey, object> OptionsSet(
659761
/// <summary>
660762
/// Tests all the code actions for the given <paramref name="input"/> string. Each code
661763
/// action must produce the corresponding output in the <paramref name="outputs"/> array.
662-
///
764+
///
663765
/// Will throw if there are more outputs than code actions or more code actions than outputs.
664766
/// </summary>
665767
protected Task TestAllInRegularAndScriptAsync(

src/EditorFeatures/TestUtilities/Diagnostics/AbstractDiagnosticProviderBasedUserDiagnosticTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ internal async override Task<IEnumerable<Diagnostic>> GetDiagnosticsAsync(
141141
}
142142

143143
var testDriver = new TestDiagnosticAnalyzerDriver(document.Project, provider);
144-
var diagnostics = (await testDriver.GetAllDiagnosticsAsync(document, span)).ToImmutableArray();
144+
var filterSpan = parameters.includeDiagnosticsOutsideSelection ? (TextSpan?)null : span;
145+
var diagnostics = (await testDriver.GetAllDiagnosticsAsync(document, filterSpan)).ToImmutableArray();
145146
AssertNoAnalyzerExceptionDiagnostics(diagnostics);
146147

147148
var fixer = providerAndFixer.Item2;

src/EditorFeatures/TestUtilities/Diagnostics/TestDiagnosticAnalyzerDriver.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ private TestDiagnosticAnalyzerService CreateDiagnosticAnalyzerService(Project pr
5454
private async Task<IEnumerable<Diagnostic>> GetDiagnosticsAsync(
5555
Project project,
5656
Document document,
57-
TextSpan span,
57+
TextSpan? filterSpan,
5858
bool getDocumentDiagnostics,
5959
bool getProjectDiagnostics)
6060
{
@@ -66,7 +66,12 @@ private async Task<IEnumerable<Diagnostic>> GetDiagnosticsAsync(
6666
if (getDocumentDiagnostics)
6767
{
6868
var dxs = await _diagnosticAnalyzerService.GetDiagnosticsAsync(project.Solution, project.Id, document.Id, _includeSuppressedDiagnostics);
69-
documentDiagnostics = await CodeAnalysis.Diagnostics.Extensions.ToDiagnosticsAsync(dxs.Where(d => d.HasTextSpan && d.GetTextSpan().IntersectsWith(span)), project, CancellationToken.None);
69+
documentDiagnostics = await CodeAnalysis.Diagnostics.Extensions.ToDiagnosticsAsync(
70+
filterSpan is null
71+
? dxs.Where(d => d.HasTextSpan)
72+
: dxs.Where(d => d.HasTextSpan && d.GetTextSpan().IntersectsWith(filterSpan.Value)),
73+
project,
74+
CancellationToken.None);
7075
}
7176

7277
if (getProjectDiagnostics)
@@ -86,9 +91,9 @@ private async Task<IEnumerable<Diagnostic>> GetDiagnosticsAsync(
8691
return allDiagnostics;
8792
}
8893

89-
public Task<IEnumerable<Diagnostic>> GetAllDiagnosticsAsync(Document document, TextSpan span)
94+
public Task<IEnumerable<Diagnostic>> GetAllDiagnosticsAsync(Document document, TextSpan? filterSpan)
9095
{
91-
return GetDiagnosticsAsync(document.Project, document, span, getDocumentDiagnostics: true, getProjectDiagnostics: true);
96+
return GetDiagnosticsAsync(document.Project, document, filterSpan, getDocumentDiagnostics: true, getProjectDiagnostics: true);
9297
}
9398

9499
public async Task<IEnumerable<Diagnostic>> GetAllDiagnosticsAsync(Project project)
@@ -125,7 +130,7 @@ public Task<IEnumerable<Diagnostic>> GetDocumentDiagnosticsAsync(Document docume
125130

126131
public Task<IEnumerable<Diagnostic>> GetProjectDiagnosticsAsync(Project project)
127132
{
128-
return GetDiagnosticsAsync(project, document: null, span: default, getDocumentDiagnostics: false, getProjectDiagnostics: true);
133+
return GetDiagnosticsAsync(project, document: null, filterSpan: default, getDocumentDiagnostics: false, getProjectDiagnostics: true);
129134
}
130135

131136
private async Task SynchronizeGlobalAssetToRemoteHostIfNeededAsync(Workspace workspace)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
3+
using System.Composition;
4+
using System.Text;
5+
using Microsoft.CodeAnalysis.CodeFixes;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
using Microsoft.CodeAnalysis.SimplifyInterpolation;
8+
9+
namespace Microsoft.CodeAnalysis.CSharp.SimplifyInterpolation
10+
{
11+
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
12+
internal class CSharpSimplifyInterpolationCodeFixProvider : AbstractSimplifyInterpolationCodeFixProvider<
13+
InterpolationSyntax, ExpressionSyntax, InterpolationAlignmentClauseSyntax,
14+
InterpolationFormatClauseSyntax, InterpolatedStringExpressionSyntax>
15+
{
16+
protected override InterpolationSyntax WithExpression(InterpolationSyntax interpolation, ExpressionSyntax expression)
17+
=> interpolation.WithExpression(expression);
18+
19+
protected override InterpolationSyntax WithAlignmentClause(InterpolationSyntax interpolation, InterpolationAlignmentClauseSyntax alignmentClause)
20+
=> interpolation.WithAlignmentClause(alignmentClause);
21+
22+
protected override InterpolationSyntax WithFormatClause(InterpolationSyntax interpolation, InterpolationFormatClauseSyntax formatClause)
23+
=> interpolation.WithFormatClause(formatClause);
24+
25+
protected override string Escape(InterpolatedStringExpressionSyntax interpolatedString, string formatString)
26+
{
27+
var result = new StringBuilder();
28+
if (interpolatedString.StringStartToken.Kind() == SyntaxKind.InterpolatedVerbatimStringStartToken)
29+
{
30+
foreach (var c in formatString)
31+
{
32+
// in a verbatim string, the only char we have to escape is the double-quote char
33+
if (c == '"')
34+
{
35+
result.Append(c);
36+
}
37+
38+
result.Append(c);
39+
}
40+
}
41+
else
42+
{
43+
// In a normal string we have to escape quotes and we have to escape an
44+
// escape character itself.
45+
foreach (var c in formatString)
46+
{
47+
if (c == '"' || c == '\\')
48+
{
49+
result.Append('\\');
50+
}
51+
52+
result.Append(c);
53+
}
54+
}
55+
56+
return result.ToString();
57+
}
58+
}
59+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
3+
using Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages.VirtualChars;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;
7+
using Microsoft.CodeAnalysis.SimplifyInterpolation;
8+
9+
namespace Microsoft.CodeAnalysis.CSharp.SimplifyInterpolation
10+
{
11+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
12+
internal class CSharpSimplifyInterpolationDiagnosticAnalyzer : AbstractSimplifyInterpolationDiagnosticAnalyzer<
13+
InterpolationSyntax, ExpressionSyntax>
14+
{
15+
protected override IVirtualCharService GetVirtualCharService()
16+
=> CSharpVirtualCharService.Instance;
17+
}
18+
}

src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ internal static class IDEDiagnosticIds
115115

116116
public const string UseSystemHashCode = "IDE0070";
117117

118+
public const string SimplifyInterpolationId = "IDE0071";
119+
118120
public const string PopulateSwitchExpressionDiagnosticId = "IDE0072";
119121

120122
// Analyzer error Ids

0 commit comments

Comments
 (0)