| File: src\roslyn\src\Analyzers\Core\CodeFixes\AliasAmbiguousType\AbstractAliasAmbiguousTypeCodeFixProvider.cs | Web Access |
| Project: Microsoft.CodeAnalysis.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CodeStyle.Fixes) |
// 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.Threading.Tasks; using Microsoft.CodeAnalysis.AddImport; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Collections; 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 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, async cancellationToken => { var aliasDirective = syntaxGenerator.AliasImportDeclaration(typeName, symbol); var newRoot = addImportService.AddImport(semanticModel, root, diagnosticNode, aliasDirective, syntaxGenerator, placementOption, cancellationToken); return 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 the 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 is INamedTypeSymbol { Arity: 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); } } }