|
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.ConvertNamespace;
using static CSharpSyntaxTokens;
using static SyntaxFactory;
internal static class ConvertNamespaceTransform
{
public static Task<Document> ConvertAsync(Document document, BaseNamespaceDeclarationSyntax baseNamespace, CSharpSyntaxFormattingOptions options, CancellationToken cancellationToken)
=> baseNamespace switch
{
FileScopedNamespaceDeclarationSyntax fileScopedNamespace => ConvertFileScopedNamespaceAsync(document, fileScopedNamespace, options, cancellationToken),
NamespaceDeclarationSyntax namespaceDeclaration => ConvertNamespaceDeclarationAsync(document, namespaceDeclaration, options, cancellationToken),
_ => throw ExceptionUtilities.UnexpectedValue(baseNamespace.Kind()),
};
/// <summary>
/// Asynchronous implementation for code fixes.
/// </summary>
public static async Task<Document> ConvertNamespaceDeclarationAsync(Document document, NamespaceDeclarationSyntax namespaceDeclaration, SyntaxFormattingOptions options, CancellationToken cancellationToken)
{
var parsedDocument = await ParsedDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false);
// Replace the block namespace with the file scoped namespace.
var annotation = new SyntaxAnnotation();
var (updatedRoot, _) = ReplaceWithFileScopedNamespace(parsedDocument, namespaceDeclaration, annotation);
var updatedDocument = document.WithSyntaxRoot(updatedRoot);
// Determine how much indentation we had inside the original block namespace. We'll attempt to remove
// that much indentation from each applicable line after we conver the block namespace to a file scoped
// namespace.
var indentation = GetIndentation(parsedDocument, namespaceDeclaration, options, cancellationToken);
if (indentation == null)
return updatedDocument;
// Now, find the file scoped namespace in the updated doc and go and dedent every line if applicable.
var updatedParsedDocument = await ParsedDocument.CreateAsync(updatedDocument, cancellationToken).ConfigureAwait(false);
var (dedentedText, _) = DedentNamespace(updatedParsedDocument, indentation, annotation, cancellationToken);
return document.WithText(dedentedText);
}
/// <summary>
/// Synchronous implementation for a command handler.
/// </summary>
public static (SourceText text, TextSpan semicolonSpan) ConvertNamespaceDeclaration(ParsedDocument document, NamespaceDeclarationSyntax namespaceDeclaration, SyntaxFormattingOptions options, CancellationToken cancellationToken)
{
// Replace the block namespace with the file scoped namespace.
var annotation = new SyntaxAnnotation();
var (updatedRoot, semicolonSpan) = ReplaceWithFileScopedNamespace(document, namespaceDeclaration, annotation);
var updatedDocument = document.WithChangedRoot(updatedRoot, cancellationToken);
// Determine how much indentation we had inside the original block namespace. We'll attempt to remove
// that much indentation from each applicable line after we conver the block namespace to a file scoped
// namespace.
var indentation = GetIndentation(document, namespaceDeclaration, options, cancellationToken);
if (indentation == null)
return (updatedDocument.Text, semicolonSpan);
// Now, find the file scoped namespace in the updated doc and go and dedent every line if applicable.
return DedentNamespace(updatedDocument, indentation, annotation, cancellationToken);
}
private static (SyntaxNode root, TextSpan semicolonSpan) ReplaceWithFileScopedNamespace(
ParsedDocument document, NamespaceDeclarationSyntax namespaceDeclaration, SyntaxAnnotation annotation)
{
var converted = ConvertNamespaceDeclaration(namespaceDeclaration);
var updatedRoot = document.Root.ReplaceNode(
namespaceDeclaration,
converted.WithAdditionalAnnotations(annotation));
var fileScopedNamespace = (FileScopedNamespaceDeclarationSyntax)updatedRoot.GetAnnotatedNodes(annotation).Single();
return (updatedRoot, fileScopedNamespace.SemicolonToken.Span);
}
private static string? GetIndentation(ParsedDocument document, NamespaceDeclarationSyntax namespaceDeclaration, SyntaxFormattingOptions options, CancellationToken cancellationToken)
{
var openBraceLine = document.Text.Lines.GetLineFromPosition(namespaceDeclaration.OpenBraceToken.SpanStart).LineNumber;
var closeBraceLine = document.Text.Lines.GetLineFromPosition(namespaceDeclaration.CloseBraceToken.SpanStart).LineNumber;
if (openBraceLine == closeBraceLine)
return null;
// Auto-formatting options are not relevant since they only control behavior on typing.
var indentationOptions = new IndentationOptions(options);
var indentationService = document.LanguageServices.GetRequiredService<IIndentationService>();
var indentation = indentationService.GetIndentation(document, openBraceLine + 1, indentationOptions, cancellationToken);
return indentation.GetIndentationString(document.Text, options.UseTabs, options.TabSize);
}
private static (SourceText text, TextSpan semicolonSpan) DedentNamespace(
ParsedDocument document, string indentation, SyntaxAnnotation annotation, CancellationToken cancellationToken)
{
var syntaxTree = document.SyntaxTree;
var text = document.Text;
var root = document.Root;
var fileScopedNamespace = (FileScopedNamespaceDeclarationSyntax)root.GetAnnotatedNodes(annotation).Single();
var semicolonLine = text.Lines.GetLineFromPosition(fileScopedNamespace.SemicolonToken.SpanStart).LineNumber;
// Cache what we compute so we don't have to recompute it for every line of a raw string literal.
(SyntaxNode stringNode, int closeTerminatorIndentationLength) lastRawStringLiteralData = default;
using var _ = ArrayBuilder<TextChange>.GetInstance(out var changes);
for (var line = semicolonLine + 1; line < text.Lines.Count; line++)
changes.AddIfNotNull(TryDedentLine(text.Lines[line]));
var dedentedText = text.WithChanges(changes);
return (dedentedText, fileScopedNamespace.SemicolonToken.Span);
TextChange? TryDedentLine(TextLine textLine)
{
// if this line is inside a string-literal or interpolated-text-content, then we definitely do not want to
// touch what is inside there. Note: this will not apply to raw-string literals, which can potentially be
// dedented safely depending on the position of their close terminator.
if (syntaxTree.IsEntirelyWithinStringLiteral(textLine.Span.Start, out var stringLiteral, cancellationToken))
{
SyntaxNode stringNode;
if (stringLiteral.Kind() is SyntaxKind.InterpolatedStringTextToken)
{
if (stringLiteral.GetRequiredParent() is not InterpolatedStringTextSyntax { Parent: InterpolatedStringExpressionSyntax { StringStartToken: (kind: SyntaxKind.InterpolatedMultiLineRawStringStartToken) } interpolatedString })
return null;
stringNode = interpolatedString;
}
else if (stringLiteral.Kind() is SyntaxKind.InterpolatedRawStringEndToken)
{
if (stringLiteral.GetRequiredParent() is not InterpolatedStringExpressionSyntax { StringStartToken: (kind: SyntaxKind.InterpolatedMultiLineRawStringStartToken) } interpolatedString)
return null;
stringNode = interpolatedString;
}
else if (stringLiteral.Kind() is SyntaxKind.MultiLineRawStringLiteralToken or SyntaxKind.Utf8MultiLineRawStringLiteralToken)
{
stringNode = stringLiteral.GetRequiredParent();
}
else
{
return null;
}
// Don't touch the raw string if it already has issues.
if (stringNode.ContainsDiagnostics)
return null;
// Ok, only dedent the raw string contents if we can dedent the closing terminator of the raw string.
if (lastRawStringLiteralData.stringNode != stringNode)
lastRawStringLiteralData = (stringNode, ComputeCommonIndentationLength(text.Lines.GetLineFromPosition(stringNode.Span.End)));
// If we can't dedent the close terminator the right amount, don't dedent any contents.
if (lastRawStringLiteralData.closeTerminatorIndentationLength != indentation.Length)
return null;
}
// Determine the amount of indentation this text line starts with.
return new TextChange(
new TextSpan(textLine.Start, ComputeCommonIndentationLength(textLine)),
newText: "");
}
int ComputeCommonIndentationLength(TextLine textLine)
{
var commonIndentation = 0;
while (commonIndentation < indentation.Length && commonIndentation < textLine.Span.Length)
{
if (indentation[commonIndentation] != text[textLine.Start + commonIndentation])
break;
commonIndentation++;
}
return commonIndentation;
}
}
private static SourceText IndentNamespace(
ParsedDocument document, string indentation, SyntaxAnnotation annotation, CancellationToken cancellationToken)
{
var syntaxTree = document.SyntaxTree;
var text = document.Text;
var root = document.Root;
var blockScopedNamespace = (NamespaceDeclarationSyntax)root.GetAnnotatedNodes(annotation).Single();
var openBraceLine = text.Lines.GetLineFromPosition(blockScopedNamespace.OpenBraceToken.SpanStart).LineNumber;
var closeBraceLine = text.Lines.GetLineFromPosition(blockScopedNamespace.CloseBraceToken.SpanStart).LineNumber;
using var _ = ArrayBuilder<TextChange>.GetInstance(Math.Max(0, closeBraceLine - openBraceLine - 1), out var changes);
for (var line = openBraceLine + 1; line < closeBraceLine; line++)
changes.AddIfNotNull(TryIndentLine(syntaxTree, root, indentation, text.Lines[line], cancellationToken));
var dedentedText = text.WithChanges(changes);
return dedentedText;
}
private static TextChange? TryIndentLine(
SyntaxTree tree, SyntaxNode root, string indentation, TextLine textLine, CancellationToken cancellationToken)
{
if (textLine.IsEmptyOrWhitespace())
return null;
// if this line is inside a string-literal or interpolated-text-content, then we definitely do not want to
// touch what is inside there. Note: this will not apply to raw-string literals, which can be indented
// safely.
if (tree.IsEntirelyWithinStringLiteral(textLine.Span.Start, cancellationToken))
return null;
if (textLine.Text![textLine.Start] == '#')
{
var token = root.FindToken(textLine.Start, findInsideTrivia: true);
if (token.IsKind(SyntaxKind.HashToken) && token.Parent!.Kind() is not (SyntaxKind.RegionDirectiveTrivia or SyntaxKind.EndRegionDirectiveTrivia))
{
// only #region and #endregion get indented
return null;
}
}
return new TextChange(new TextSpan(textLine.Start, 0), newText: indentation);
}
public static async Task<Document> ConvertFileScopedNamespaceAsync(
Document document, FileScopedNamespaceDeclarationSyntax fileScopedNamespace, CSharpSyntaxFormattingOptions options, CancellationToken cancellationToken)
{
var parsedDocument = await ParsedDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false);
// Replace the block namespace with the file scoped namespace.
var annotation = new SyntaxAnnotation();
var updatedRoot = ReplaceWithBlockScopedNamespace(parsedDocument, fileScopedNamespace, options.NewLine, options.NewLines, annotation);
var updatedDocument = document.WithSyntaxRoot(updatedRoot);
// Auto-formatting options are not relevant since they only control behavior on typing.
var indentation = FormattingExtensions.CreateIndentationString(options.IndentationSize, options.UseTabs, options.TabSize);
if (indentation == "")
return updatedDocument;
// Now, find the file scoped namespace in the updated doc and go and dedent every line if applicable.
var updatedParsedDocument = await ParsedDocument.CreateAsync(updatedDocument, cancellationToken).ConfigureAwait(false);
var indentedText = IndentNamespace(updatedParsedDocument, indentation, annotation, cancellationToken);
return document.WithText(indentedText);
}
private static SyntaxNode ReplaceWithBlockScopedNamespace(
ParsedDocument document, FileScopedNamespaceDeclarationSyntax namespaceDeclaration, string lineEnding, NewLinePlacement newLinePlacement, SyntaxAnnotation annotation)
{
var converted = ConvertFileScopedNamespace(document, namespaceDeclaration, lineEnding, newLinePlacement);
// If the leading trivia of the token after the end of the file scoped namespace spans multiple lines, make
// sure all the lines preceding the line with the next token are placed inside the block body namespace.
var tokenAfterNamespace = namespaceDeclaration.GetLastToken(includeZeroWidth: true, includeSkipped: true).GetNextTokenOrEndOfFile(includeZeroWidth: true, includeSkipped: true);
var lineWithNextToken = document.Text.Lines.GetLineFromPosition(tokenAfterNamespace.SpanStart);
var (splitPosition, needsAdditionalLineEnding) = lineWithNextToken.GetFirstNonWhitespacePosition() < tokenAfterNamespace.SpanStart
? (tokenAfterNamespace.SpanStart, true)
: (document.Text.Lines.GetLineFromPosition(tokenAfterNamespace.SpanStart).Start, false);
var triviaBeforeSplit = tokenAfterNamespace.LeadingTrivia.TakeWhile(trivia => trivia.SpanStart < splitPosition).ToArray();
var triviaAfterSplit = tokenAfterNamespace.LeadingTrivia.Skip(triviaBeforeSplit.Length).ToArray();
if (triviaBeforeSplit.Length > 0)
{
if (needsAdditionalLineEnding)
triviaBeforeSplit = triviaBeforeSplit.Append(EndOfLine(lineEnding));
converted = converted.WithCloseBraceToken(converted.CloseBraceToken.WithPrependedLeadingTrivia(triviaBeforeSplit));
}
// If the block namespace starts with a blank line, remove one blank line as an adjustment relative to the
// file scoped namespace. This check is performed here to account for cases where the token after the
// opening brace is the closing brace token, and the leading newline for the closing brace token was
// introduced by the trivia relocation above.
var firstBodyToken = converted.OpenBraceToken.GetNextToken(includeZeroWidth: true, includeSkipped: true);
if (firstBodyToken.Kind() != SyntaxKind.EndOfFileToken
&& HasLeadingBlankLine(firstBodyToken, out var firstBodyTokenWithoutBlankLine))
{
converted = converted.ReplaceToken(firstBodyToken, firstBodyTokenWithoutBlankLine);
}
return document.Root.ReplaceSyntax(
[namespaceDeclaration],
(_, _) => converted.WithAdditionalAnnotations(annotation),
[tokenAfterNamespace],
(_, _) => tokenAfterNamespace.WithLeadingTrivia(triviaAfterSplit),
[],
(_, _) => throw ExceptionUtilities.Unreachable());
}
private static bool HasLeadingBlankLine(
SyntaxToken token, out SyntaxToken withoutBlankLine)
{
var leadingTrivia = token.LeadingTrivia;
if (leadingTrivia is [(kind: SyntaxKind.EndOfLineTrivia), ..])
{
withoutBlankLine = token.WithLeadingTrivia(leadingTrivia.RemoveAt(0));
return true;
}
if (leadingTrivia is [(kind: SyntaxKind.WhitespaceTrivia), (kind: SyntaxKind.EndOfLineTrivia), ..])
{
withoutBlankLine = token.WithLeadingTrivia(leadingTrivia.Skip(2));
return true;
}
withoutBlankLine = default;
return false;
}
private static FileScopedNamespaceDeclarationSyntax ConvertNamespaceDeclaration(NamespaceDeclarationSyntax namespaceDeclaration)
{
// If the open-brace token has any special trivia, then move them to after the semicolon.
var semiColon = SemicolonToken
.WithoutTrivia()
.WithTrailingTrivia(namespaceDeclaration.Name.GetTrailingTrivia())
.WithAppendedTrailingTrivia(namespaceDeclaration.OpenBraceToken.LeadingTrivia);
if (!namespaceDeclaration.OpenBraceToken.TrailingTrivia.All(static t => t.IsWhitespace()))
semiColon = semiColon.WithAppendedTrailingTrivia(namespaceDeclaration.OpenBraceToken.TrailingTrivia);
// Move trivia after the original name token to now be after the new semicolon token.
var fileScopedNamespace = FileScopedNamespaceDeclaration(
namespaceDeclaration.AttributeLists,
namespaceDeclaration.Modifiers,
namespaceDeclaration.NamespaceKeyword,
namespaceDeclaration.Name.WithoutTrailingTrivia(),
semiColon,
namespaceDeclaration.Externs,
namespaceDeclaration.Usings,
namespaceDeclaration.Members);
// Copy trivia from the close brace to the end of the file scoped namespace (which means after all of the members)
fileScopedNamespace = fileScopedNamespace
.WithAppendedTrailingTrivia(namespaceDeclaration.CloseBraceToken.LeadingTrivia)
.WithAppendedTrailingTrivia(namespaceDeclaration.CloseBraceToken.TrailingTrivia);
var originalHadTrailingNewLine = namespaceDeclaration.GetTrailingTrivia() is [.., (kind: SyntaxKind.EndOfLineTrivia)];
// now, intelligently trim excess newlines to try to match what the original namespace looked like.
while (fileScopedNamespace.HasTrailingTrivia)
{
var trailingTrivia = fileScopedNamespace.GetTrailingTrivia();
// if the new namespace doesn't end with a newline, nothing for us to do.
if (trailingTrivia is not [.., (kind: SyntaxKind.EndOfLineTrivia)])
break;
// if the original had a newline, then we only want to trim the newlines as long as there is still one
// left at the end.
if (originalHadTrailingNewLine && trailingTrivia is not
[
..,
(kind: SyntaxKind.EndOfLineTrivia or SyntaxKind.EndIfDirectiveTrivia or SyntaxKind.EndRegionDirectiveTrivia),
(kind: SyntaxKind.EndOfLineTrivia)
])
{
break;
}
// New namespace has excess newlines, remove the last one and try again.
fileScopedNamespace = fileScopedNamespace.WithTrailingTrivia(
trailingTrivia.Take(trailingTrivia.Count - 1));
}
return fileScopedNamespace;
}
private static NamespaceDeclarationSyntax ConvertFileScopedNamespace(ParsedDocument document, FileScopedNamespaceDeclarationSyntax fileScopedNamespace, string lineEnding, NewLinePlacement newLinePlacement)
{
var nameSyntax = fileScopedNamespace.Name.WithAppendedTrailingTrivia(fileScopedNamespace.SemicolonToken.LeadingTrivia)
.WithAppendedTrailingTrivia(newLinePlacement.HasFlag(NewLinePlacement.BeforeOpenBraceInTypes) ? EndOfLine(lineEnding) : Space);
var openBraceToken = OpenBraceToken.WithoutLeadingTrivia().WithTrailingTrivia(fileScopedNamespace.SemicolonToken.TrailingTrivia);
if (openBraceToken.TrailingTrivia is not [.., SyntaxTrivia(SyntaxKind.EndOfLineTrivia)])
{
openBraceToken = openBraceToken.WithAppendedTrailingTrivia(EndOfLine(lineEnding));
}
FileScopedNamespaceDeclarationSyntax adjustedFileScopedNamespace;
var closeBraceToken = CloseBraceToken.WithoutLeadingTrivia().WithoutTrailingTrivia();
// Normally the block scoped namespace will have a newline after the closing brace. The only exception to
// this occurs when there are no tokens after the closing brace and the document with a file scoped
// namespace did not end in a trailing newline. For this case, we want the converted block scope namespace
// to also terminate without a final newline.
if (!fileScopedNamespace.GetLastToken().GetNextTokenOrEndOfFile().IsKind(SyntaxKind.EndOfFileToken)
|| document.Text.Lines.GetLinePosition(document.Text.Length).Character == 0)
{
closeBraceToken = closeBraceToken.WithAppendedTrailingTrivia(EndOfLine(lineEnding));
adjustedFileScopedNamespace = fileScopedNamespace;
}
else
{
// Make sure the body of the file scoped namespace ends with a trailing new line (so the closing brace
// of the converted block-body namespace appears on its own line), but don't add a new line after the
// closing brace.
adjustedFileScopedNamespace = fileScopedNamespace.WithAppendedTrailingTrivia(EndOfLine(lineEnding));
}
// If the file scoped namespace is indented, also indent the newly added braces to match
var outerIndentation = document.Text.GetLeadingWhitespaceOfLineAtPosition(fileScopedNamespace.SpanStart);
if (outerIndentation.Length > 0)
{
if (newLinePlacement.HasFlag(NewLinePlacement.BeforeOpenBraceInTypes))
openBraceToken = openBraceToken.WithLeadingTrivia(openBraceToken.LeadingTrivia.Add(Whitespace(outerIndentation)));
closeBraceToken = closeBraceToken.WithLeadingTrivia(closeBraceToken.LeadingTrivia.Add(Whitespace(outerIndentation)));
}
var namespaceDeclaration = NamespaceDeclaration(
adjustedFileScopedNamespace.AttributeLists,
adjustedFileScopedNamespace.Modifiers,
adjustedFileScopedNamespace.NamespaceKeyword,
nameSyntax,
openBraceToken,
adjustedFileScopedNamespace.Externs,
adjustedFileScopedNamespace.Usings,
adjustedFileScopedNamespace.Members,
closeBraceToken,
semicolonToken: default);
return namespaceDeclaration;
}
}
|