|
// 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.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.Utilities;
internal static partial class UsingsAndExternAliasesOrganizer
{
public static void Organize(
SyntaxList<ExternAliasDirectiveSyntax> externAliasList,
SyntaxList<UsingDirectiveSyntax> usingList,
bool placeSystemNamespaceFirst,
bool separateGroups,
SyntaxTrivia fallbackTrivia,
out SyntaxList<ExternAliasDirectiveSyntax> organizedExternAliasList,
out SyntaxList<UsingDirectiveSyntax> organizedUsingList)
{
// Attempt to use an existing newline trivia from the existing usings/externs. If we can't find any use what
// the caller passed in.
var newLineTrivia = ((IEnumerable<SyntaxNode>)externAliasList)
.Concat(usingList)
.Select(n => n.GetTrailingTrivia().FirstOrNull(t => t.Kind() == SyntaxKind.EndOfLineTrivia))
.Where(t => t != null)
.FirstOrDefault() ?? fallbackTrivia;
OrganizeWorker(
externAliasList, usingList, placeSystemNamespaceFirst,
newLineTrivia,
out organizedExternAliasList, out organizedUsingList);
if (separateGroups)
{
if (organizedExternAliasList.Count > 0 && organizedUsingList.Count > 0)
{
var firstUsing = organizedUsingList[0];
if (!firstUsing.GetLeadingTrivia().Any(t => t.IsEndOfLine()))
{
var newFirstUsing = firstUsing.WithPrependedLeadingTrivia(newLineTrivia);
organizedUsingList = organizedUsingList.Replace(firstUsing, newFirstUsing);
}
}
for (var i = 1; i < organizedUsingList.Count; i++)
{
var lastUsing = organizedUsingList[i - 1];
var currentUsing = organizedUsingList[i];
if (NeedsGrouping(lastUsing, currentUsing) &&
!currentUsing.GetLeadingTrivia().Any(t => t.IsEndOfLine()))
{
var newCurrentUsing = currentUsing.WithPrependedLeadingTrivia(newLineTrivia);
organizedUsingList = organizedUsingList.Replace(currentUsing, newCurrentUsing);
}
}
}
}
public static bool NeedsGrouping(
UsingDirectiveSyntax using1,
UsingDirectiveSyntax using2)
{
var directive1IsUsingStatic = using1.StaticKeyword.IsKind(SyntaxKind.StaticKeyword);
var directive2IsUsingStatic = using2.StaticKeyword.IsKind(SyntaxKind.StaticKeyword);
var directive1IsAlias = using1.Alias != null;
var directive2IsAlias = using2.Alias != null;
var directive1IsNamespace = !directive1IsUsingStatic && !directive1IsAlias;
var directive2IsNamespace = !directive2IsUsingStatic && !directive2IsAlias;
if (directive1IsAlias && directive2IsAlias)
{
return false;
}
if (directive1IsUsingStatic && directive2IsUsingStatic)
{
return false;
}
if (directive1IsNamespace && directive2IsNamespace)
{
// Both normal usings. Place them in groups if their first namespace
// component differs.
Contract.ThrowIfNull(using1.Name);
Contract.ThrowIfNull(using2.Name);
var name1 = using1.Name.GetFirstToken().ValueText;
var name2 = using2.Name.GetFirstToken().ValueText;
return name1 != name2;
}
// They have different types, definitely put them into new groups.
return true;
}
private static void OrganizeWorker(
SyntaxList<ExternAliasDirectiveSyntax> externAliasList,
SyntaxList<UsingDirectiveSyntax> usingList,
bool placeSystemNamespaceFirst,
SyntaxTrivia newLineTrivia,
out SyntaxList<ExternAliasDirectiveSyntax> organizedExternAliasList,
out SyntaxList<UsingDirectiveSyntax> organizedUsingList)
{
if (externAliasList.Count > 0 || usingList.Count > 0)
{
// Merge the list of usings and externs into one list.
// order them in the order that they were originally in the
// file.
var initialList = usingList.Cast<SyntaxNode>()
.Concat(externAliasList)
.OrderBy(n => n.SpanStart).ToList();
if (!initialList.SpansPreprocessorDirective())
{
// If there is a banner comment that precedes the nodes,
// then remove it and store it for later.
initialList[0] = initialList[0].GetNodeWithoutLeadingBannerAndPreprocessorDirectives(out var leadingTrivia);
var comparer = placeSystemNamespaceFirst
? UsingsAndExternAliasesDirectiveComparer.SystemFirstInstance
: UsingsAndExternAliasesDirectiveComparer.NormalInstance;
var finalList = initialList.OrderBy(comparer).ToList();
// Check if sorting the list actually changed anything. If not, then we don't
// need to make any changes to the file.
if (!finalList.SequenceEqual(initialList))
{
// Make sure newlines are correct between nodes.
EnsureNewLines(finalList, newLineTrivia);
// Reattach the banner.
finalList[0] = finalList[0].WithPrependedLeadingTrivia(leadingTrivia);
// Now split out the externs and usings back into two separate lists.
organizedExternAliasList = [.. finalList
.Where(t => t is ExternAliasDirectiveSyntax)
.Cast<ExternAliasDirectiveSyntax>()];
organizedUsingList = [.. finalList
.Where(t => t is UsingDirectiveSyntax)
.Cast<UsingDirectiveSyntax>()];
return;
}
}
}
organizedExternAliasList = externAliasList;
organizedUsingList = usingList;
}
private static void EnsureNewLines(IList<SyntaxNode> list, SyntaxTrivia newLineTrivia)
{
// First, make sure that every node (except the last one) ends with
// a newline.
for (var i = 0; i < list.Count - 1; i++)
{
var node = list[i];
var trailingTrivia = node.GetTrailingTrivia();
if (!trailingTrivia.Any() || trailingTrivia.Last().Kind() != SyntaxKind.EndOfLineTrivia)
{
list[i] = node.WithTrailingTrivia(trailingTrivia.Concat(newLineTrivia));
}
}
// Now, make sure that every node (except the first one) does *not*
// start with newlines.
for (var i = 1; i < list.Count; i++)
{
var node = list[i];
list[i] = TrimLeadingNewLines(node);
}
list[0] = TrimLeadingNewLines(list[0]);
}
private static SyntaxNode TrimLeadingNewLines(SyntaxNode node)
=> node.WithLeadingTrivia(node.GetLeadingTrivia().SkipWhile(t => t.Kind() == SyntaxKind.EndOfLineTrivia));
}
|