|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
namespace Microsoft.CodeAnalysis.CSharp.NewLines.EmbeddedStatementPlacement;
using static SyntaxFactory;
[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.EmbeddedStatementPlacement), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class EmbeddedStatementPlacementCodeFixProvider() : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
=> [IDEDiagnosticIds.EmbeddedStatementPlacementDiagnosticId];
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var document = context.Document;
var diagnostic = context.Diagnostics.First();
context.RegisterCodeFix(
CodeAction.Create(
CSharpCodeFixesResources.Place_statement_on_following_line,
c => FixAllAsync(document, [diagnostic], c),
nameof(CSharpCodeFixesResources.Place_statement_on_following_line)),
context.Diagnostics);
return Task.CompletedTask;
}
public static async Task<Document> FixAllAsync(Document document, ImmutableArray<Diagnostic> diagnostics, CancellationToken cancellationToken)
{
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var editor = new SyntaxEditor(root, document.Project.Solution.Services);
var options = await document.GetCSharpSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
var endOfLineTrivia = ElasticEndOfLine(options.NewLine);
foreach (var diagnostic in diagnostics)
FixOne(editor, diagnostic, endOfLineTrivia, cancellationToken);
return document.WithSyntaxRoot(editor.GetChangedRoot());
}
private static void FixOne(
SyntaxEditor editor,
Diagnostic diagnostic,
SyntaxTrivia endOfLineTrivia,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var root = editor.OriginalRoot;
var node = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan);
if (node is not StatementSyntax startStatement)
{
Debug.Fail("Couldn't find statement in fixer");
return;
}
// fixup this statement and all nested statements that have an issue.
var descendentStatements = startStatement.DescendantNodesAndSelf().OfType<StatementSyntax>();
var badStatements = descendentStatements.Where(s => EmbeddedStatementPlacementDiagnosticAnalyzer.StatementNeedsWrapping(s));
// Walk from lower statements to higher so the higher up changes see the changes below.
foreach (var badStatement in badStatements.OrderByDescending(s => s.SpanStart))
{
editor.ReplaceNode(
badStatement,
(currentBadStatement, _) =>
{
// Ensure a newline between the statement and the statement that preceded it.
var updatedStatement = AddLeadingTrivia(currentBadStatement, endOfLineTrivia);
// Ensure that if we wrap an empty block that the trailing brace is on a new line as well.
if (updatedStatement is BlockSyntax blockSyntax &&
blockSyntax.Statements.Count == 0)
{
updatedStatement = blockSyntax.WithCloseBraceToken(
AddLeadingTrivia(blockSyntax.CloseBraceToken, ElasticMarker));
}
return updatedStatement;
});
}
// Now walk up all our containing blocks ensuring that they wrap over multiple lines
var ancestorBlocks = startStatement.AncestorsAndSelf().OfType<BlockSyntax>();
foreach (var block in ancestorBlocks)
{
var openBrace = block.OpenBraceToken;
var previousToken = openBrace.GetPreviousToken();
editor.ReplaceNode(
block,
(current, _) =>
{
// If the block's open { is not already on a new line, add an elastic marker so it will be placed there.
var currentBlock = (BlockSyntax)current;
if (!EmbeddedStatementPlacementDiagnosticAnalyzer.ContainsEndOfLineBetween(previousToken, openBrace))
{
currentBlock = currentBlock.WithOpenBraceToken(
AddLeadingTrivia(currentBlock.OpenBraceToken, ElasticMarker));
}
return currentBlock.WithCloseBraceToken(
AddLeadingTrivia(currentBlock.CloseBraceToken, ElasticMarker));
});
}
}
private static SyntaxNode AddLeadingTrivia(SyntaxNode node, SyntaxTrivia trivia)
=> node.WithLeadingTrivia(node.GetLeadingTrivia().Insert(0, trivia));
private static SyntaxToken AddLeadingTrivia(SyntaxToken token, SyntaxTrivia trivia)
=> token.WithLeadingTrivia(token.LeadingTrivia.Insert(0, trivia));
public override FixAllProvider GetFixAllProvider()
=> FixAllProvider.Create(
async (context, document, diagnostics) => await FixAllAsync(document, diagnostics, context.CancellationToken).ConfigureAwait(false));
}
|