|
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LineSeparators;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.LineSeparators;
[ExportLanguageService(typeof(ILineSeparatorService), LanguageNames.CSharp), Shared]
internal class CSharpLineSeparatorService : ILineSeparatorService
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CSharpLineSeparatorService()
{
}
/// <summary>
/// Given a tree returns line separator spans.
/// The operation may take fairly long time on a big tree so it is cancellable.
/// </summary>
public async Task<ImmutableArray<TextSpan>> GetLineSeparatorsAsync(
Document document,
TextSpan textSpan,
CancellationToken cancellationToken)
{
var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var node = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false);
using var _ = ArrayBuilder<TextSpan>.GetInstance(out var spans);
var blocks = node.Traverse<SyntaxNode>(textSpan, IsSeparableContainer);
foreach (var block in blocks)
{
if (cancellationToken.IsCancellationRequested)
return [];
switch (block)
{
case TypeDeclarationSyntax typeBlock:
ProcessNodeList(typeBlock.Members, spans, cancellationToken);
continue;
case BaseNamespaceDeclarationSyntax namespaceBlock:
ProcessUsings(namespaceBlock.Usings, spans, cancellationToken);
ProcessNodeList(namespaceBlock.Members, spans, cancellationToken);
continue;
case CompilationUnitSyntax progBlock:
ProcessUsings(progBlock.Usings, spans, cancellationToken);
ProcessNodeList(progBlock.Members, spans, cancellationToken);
break;
}
}
return spans.ToImmutableAndClear();
}
/// <summary>Node types that are interesting for line separation.</summary>
private static bool IsSeparableBlock(SyntaxNode node)
{
if (SyntaxFacts.IsTypeDeclaration(node.Kind()))
{
return true;
}
switch (node.Kind())
{
case SyntaxKind.NamespaceDeclaration:
case SyntaxKind.MethodDeclaration:
case SyntaxKind.PropertyDeclaration:
case SyntaxKind.EventDeclaration:
case SyntaxKind.IndexerDeclaration:
case SyntaxKind.ConstructorDeclaration:
case SyntaxKind.DestructorDeclaration:
case SyntaxKind.OperatorDeclaration:
case SyntaxKind.ConversionOperatorDeclaration:
return true;
default:
return false;
}
}
/// <summary>Node types that may contain separable blocks.</summary>
private static bool IsSeparableContainer(SyntaxNode node)
=> node is TypeDeclarationSyntax or BaseNamespaceDeclarationSyntax or CompilationUnitSyntax;
private static bool IsBadType(SyntaxNode node)
{
if (node is TypeDeclarationSyntax typeDecl)
{
if (typeDecl.OpenBraceToken.IsMissing ||
typeDecl.CloseBraceToken.IsMissing)
{
return true;
}
}
return false;
}
private static bool IsBadEnum(SyntaxNode node)
{
if (node is EnumDeclarationSyntax enumDecl)
{
if (enumDecl.OpenBraceToken.IsMissing ||
enumDecl.CloseBraceToken.IsMissing)
{
return true;
}
}
return false;
}
private static bool IsBadMethod(SyntaxNode node)
{
if (node is MethodDeclarationSyntax methodDecl)
{
if (methodDecl.Body != null &&
(methodDecl.Body.OpenBraceToken.IsMissing ||
methodDecl.Body.CloseBraceToken.IsMissing))
{
return true;
}
}
return false;
}
private static bool IsBadProperty(SyntaxNode node)
=> IsBadAccessorList(node as PropertyDeclarationSyntax);
private static bool IsBadEvent(SyntaxNode node)
=> IsBadAccessorList(node as EventDeclarationSyntax);
private static bool IsBadIndexer(SyntaxNode node)
=> IsBadAccessorList(node as IndexerDeclarationSyntax);
private static bool IsBadAccessorList(BasePropertyDeclarationSyntax? baseProperty)
{
if (baseProperty?.AccessorList == null)
return false;
return baseProperty.AccessorList.OpenBraceToken.IsMissing ||
baseProperty.AccessorList.CloseBraceToken.IsMissing;
}
private static bool IsBadConstructor(SyntaxNode node)
{
if (node is ConstructorDeclarationSyntax constructorDecl)
{
if (constructorDecl.Body != null &&
(constructorDecl.Body.OpenBraceToken.IsMissing ||
constructorDecl.Body.CloseBraceToken.IsMissing))
{
return true;
}
}
return false;
}
private static bool IsBadDestructor(SyntaxNode node)
{
if (node is DestructorDeclarationSyntax destructorDecl)
{
if (destructorDecl.Body != null &&
(destructorDecl.Body.OpenBraceToken.IsMissing ||
destructorDecl.Body.CloseBraceToken.IsMissing))
{
return true;
}
}
return false;
}
private static bool IsBadOperator(SyntaxNode node)
{
if (node is OperatorDeclarationSyntax operatorDecl)
{
if (operatorDecl.Body != null &&
(operatorDecl.Body.OpenBraceToken.IsMissing ||
operatorDecl.Body.CloseBraceToken.IsMissing))
{
return true;
}
}
return false;
}
private static bool IsBadConversionOperator(SyntaxNode node)
{
if (node is ConversionOperatorDeclarationSyntax conversionDecl)
{
if (conversionDecl.Body != null &&
(conversionDecl.Body.OpenBraceToken.IsMissing ||
conversionDecl.Body.CloseBraceToken.IsMissing))
{
return true;
}
}
return false;
}
private static bool IsBadNode(SyntaxNode node)
{
if (node is IncompleteMemberSyntax)
{
return true;
}
if (IsBadType(node) ||
IsBadEnum(node) ||
IsBadMethod(node) ||
IsBadProperty(node) ||
IsBadEvent(node) ||
IsBadIndexer(node) ||
IsBadConstructor(node) ||
IsBadDestructor(node) ||
IsBadOperator(node) ||
IsBadConversionOperator(node))
{
return true;
}
return false;
}
private static void ProcessUsings(SyntaxList<UsingDirectiveSyntax> usings, ArrayBuilder<TextSpan> spans, CancellationToken cancellationToken)
{
Contract.ThrowIfNull(spans);
if (usings.Any())
{
AddLineSeparatorSpanForNode(usings.Last(), spans, cancellationToken);
}
}
/// <summary>
/// If node is separable and not the last in its container => add line separator after the node
/// If node is separable and not the first in its container => ensure separator before the node
/// last separable node in Program needs separator after it.
/// </summary>
private static void ProcessNodeList<T>(SyntaxList<T> children, ArrayBuilder<TextSpan> spans, CancellationToken cancellationToken) where T : SyntaxNode
{
Contract.ThrowIfNull(spans);
if (children.Count == 0)
{
// nothing to separate
return;
}
// first child needs no separator
var seenSeparator = true;
for (var i = 0; i < children.Count - 1; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var cur = children[i];
if (!IsSeparableBlock(cur))
{
seenSeparator = false;
}
else
{
if (!seenSeparator)
{
var prev = children[i - 1];
AddLineSeparatorSpanForNode(prev, spans, cancellationToken);
}
AddLineSeparatorSpanForNode(cur, spans, cancellationToken);
seenSeparator = true;
}
}
// last child may need separator only before it
var lastChild = children.Last();
if (IsSeparableBlock(lastChild))
{
if (!seenSeparator)
{
var nextToLast = children[^2];
AddLineSeparatorSpanForNode(nextToLast, spans, cancellationToken);
}
if (lastChild.IsParentKind(SyntaxKind.CompilationUnit))
{
AddLineSeparatorSpanForNode(lastChild, spans, cancellationToken);
}
}
}
private static void AddLineSeparatorSpanForNode(SyntaxNode node, ArrayBuilder<TextSpan> spans, CancellationToken cancellationToken)
{
if (IsBadNode(node))
{
return;
}
var span = GetLineSeparatorSpanForNode(node);
if (IsLegalSpanForLineSeparator(node.SyntaxTree, span, cancellationToken))
{
spans.Add(span);
}
}
private static bool IsLegalSpanForLineSeparator(SyntaxTree syntaxTree, TextSpan textSpan, CancellationToken cancellationToken)
{
// A span is a legal location for a line separator if the following line
// contains only whitespace or the span is the last line in the buffer.
var line = syntaxTree.GetText(cancellationToken).Lines.IndexOf(textSpan.End);
if (line == syntaxTree.GetText(cancellationToken).Lines.Count - 1)
{
return true;
}
if (string.IsNullOrWhiteSpace(syntaxTree.GetText(cancellationToken).Lines[line + 1].ToString()))
{
return true;
}
return false;
}
private static TextSpan GetLineSeparatorSpanForNode(SyntaxNode node)
{
// we only want to underline the node with a long line
// for this purpose the last token is as good as the whole node, but has
// simpler and typically single line geometry (so it will be easier to find "bottom")
return node.GetLastToken().Span;
}
}
|