File: src\Analyzers\Core\CodeFixes\AliasAmbiguousType\AbstractAliasAmbiguousTypeCodeFixProvider.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.AddImport;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.AliasAmbiguousType;
 
internal abstract class AbstractAliasAmbiguousTypeCodeFixProvider : CodeFixProvider
{
    protected abstract string GetTextPreviewOfChange(string aliasName, ITypeSymbol typeSymbol);
 
    public override FixAllProvider? GetFixAllProvider() => null;
 
    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var cancellationToken = context.CancellationToken;
        var document = context.Document;
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        // Innermost: We are looking for an IdentifierName. IdentifierName is sometimes at the same span as its parent (e.g. SimpleBaseTypeSyntax).
        var diagnosticNode = root.FindNode(context.Span, getInnermostNodeForTie: true);
        if (!syntaxFacts.IsIdentifierName(diagnosticNode))
            return;
 
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var symbolInfo = semanticModel.GetSymbolInfo(diagnosticNode, cancellationToken);
        if (!SymbolCandidatesContainsSupportedSymbols(symbolInfo))
            return;
 
        var addImportService = document.GetRequiredLanguageService<IAddImportsService>();
        var syntaxGenerator = document.GetRequiredLanguageService<SyntaxGenerator>();
        var compilation = semanticModel.Compilation;
 
        var placementOption = await document.GetAddImportPlacementOptionsAsync(cancellationToken).ConfigureAwait(false);
 
        using var _ = ArrayBuilder<CodeAction>.GetInstance(out var actions);
        foreach (var symbol in Sort(symbolInfo.CandidateSymbols.Cast<ITypeSymbol>(), placementOption.PlaceSystemNamespaceFirst))
        {
            var typeName = symbol.Name;
            var title = GetTextPreviewOfChange(typeName, symbol);
 
            actions.Add(CodeAction.Create(
                title,
                cancellationToken =>
                {
                    var aliasDirective = syntaxGenerator.AliasImportDeclaration(typeName, symbol);
                    var newRoot = addImportService.AddImport(compilation, root, diagnosticNode, aliasDirective, syntaxGenerator, placementOption, cancellationToken);
                    return Task.FromResult(document.WithSyntaxRoot(newRoot));
                },
                title));
        }
 
        context.RegisterCodeFix(
            CodeAction.Create(
                string.Format(CodeFixesResources.Alias_ambiguous_type_0, diagnosticNode.ToString()),
                actions.ToImmutable(),
                isInlinable: true),
            context.Diagnostics.First());
    }
 
    private static IEnumerable<ITypeSymbol> Sort(IEnumerable<ITypeSymbol> types, bool sortSystemFirst)
    {
        // get all the name portions of the fully-qualified-names of the types in 'types'.
        // cache these in this local dictionary so we only have to compute them once.
        var typeToNameSegments = new Dictionary<ITypeSymbol, ImmutableArray<string>>();
 
        return types.OrderBy((t1, t2) =>
        {
            var t1NameSegments = GetNameSegments(t1);
            var t2NameSegments = GetNameSegments(t2);
 
            // compare all the name segments the two types have in common.
            for (int i = 0, n = Math.Min(t1NameSegments.Length, t2NameSegments.Length); i < n; i++)
            {
                var t1NameSegment = t1NameSegments[i];
                var t2NameSegment = t2NameSegments[i];
 
                // if we're on the first name segment, ensure we sort 'System' properly if the user
                // prefers them coming first.
                var comparer = i == 0 && sortSystemFirst ? SortSystemFirstComparer.Instance : StringComparer.Ordinal;
 
                var diff = comparer.Compare(t1NameSegment, t2NameSegment);
                if (diff != 0)
                    return diff;
            }
 
            // if all the names matched up to this point, then the type with the shorter number of segments comes first.
            return t1NameSegments.Length - t2NameSegments.Length;
        });
 
        ImmutableArray<string> GetNameSegments(ITypeSymbol symbol)
        {
            return typeToNameSegments.GetOrAdd(symbol, static symbol =>
            {
                using var result = TemporaryArray<string>.Empty;
 
                for (ISymbol current = symbol; current != null; current = current.ContainingSymbol)
                {
                    if (string.IsNullOrEmpty(current.Name))
                        break;
 
                    result.Add(current.Name);
                }
 
                // We walked upwards to get the name segments.  So reverse teh order here so it goes from outer-most to
                // inner-most names.
                result.ReverseContents();
                return result.ToImmutableAndClear();
            });
        }
    }
 
    private static bool SymbolCandidatesContainsSupportedSymbols(SymbolInfo symbolInfo)
        => symbolInfo.CandidateReason == CandidateReason.Ambiguous &&
           // Arity: Aliases can only name closed constructed types. (See also proposal https://github.com/dotnet/csharplang/issues/1239)
           // Aliasing as a closed constructed type is possible but would require to remove the type arguments from the diagnosed node.
           // It is unlikely that the user wants that and so generic types are not supported.
           symbolInfo.CandidateSymbols.All(symbol => symbol.IsKind(SymbolKind.NamedType) &&
                                                     symbol.GetArity() == 0);
 
    private sealed class SortSystemFirstComparer : IComparer<string>
    {
        public static readonly IComparer<string> Instance = new SortSystemFirstComparer();
 
        public int Compare(string? x, string? y)
        {
            var xIsSystem = x == nameof(System);
            var yIsSystem = y == nameof(System);
 
            if (xIsSystem && yIsSystem)
                return 0;
 
            if (xIsSystem)
                return -1;
 
            if (yIsSystem)
                return 1;
 
            return StringComparer.Ordinal.Compare(x, y);
        }
    }
}