File: Venus\ContainedLanguageCodeSupport.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ozsccwvc_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Editor.Undo;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.VisualStudio.LanguageServices.Implementation.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Microsoft.VisualStudio.TextManager.Interop;
using Roslyn.Utilities;
using VsTextSpan = Microsoft.VisualStudio.TextManager.Interop.TextSpan;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Venus;
 
internal static class ContainedLanguageCodeSupport
{
    public static bool IsValidId(Document document, string identifier)
    {
        var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
        return syntaxFacts.IsValidIdentifier(identifier);
    }
 
    public static bool TryGetBaseClassName(Document document, string className, CancellationToken cancellationToken, out string baseClassName)
    {
        baseClassName = null;
        var type = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetTypeByMetadataName(className);
        if (type == null || type.BaseType == null)
        {
            return false;
        }
 
        baseClassName = type.BaseType.ToDisplayString();
        return true;
    }
 
    public static string CreateUniqueEventName(
        Document document, string className, string objectName, string nameOfEvent, CancellationToken cancellationToken)
    {
        var type = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetTypeByMetadataName(className);
        var name = objectName + "_" + nameOfEvent;
 
        var semanticModel = document.GetSemanticModelAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
 
        var tree = document.GetSyntaxTreeSynchronously(cancellationToken);
        var typeNode = type.DeclaringSyntaxReferences.Where(r => r.SyntaxTree == tree).Select(r => r.GetSyntax(cancellationToken)).First();
        var codeModel = document.GetRequiredLanguageService<ICodeModelNavigationPointService>();
        var options = document.GetLineFormattingOptionsAsync(cancellationToken).AsTask().WaitAndGetResult_Venus(cancellationToken);
        var point = codeModel.GetStartPoint(typeNode, options, EnvDTE.vsCMPart.vsCMPartBody);
        var reservedNames = semanticModel.LookupSymbols(point.Value.Position, type).Select(m => m.Name);
 
        return NameGenerator.EnsureUniqueness(name, reservedNames, document.GetLanguageService<ISyntaxFactsService>().IsCaseSensitive);
    }
 
    /// <summary>
    /// Determine what methods of <paramref name=" className"/> could possibly be used as event
    /// handlers.
    /// </summary>
    /// <param name="document">The document containing <paramref name="className"/>.</param>
    /// <param name="className">The name of the type whose methods should be considered.</param>
    /// <param name="objectTypeName">The fully qualified name of the type containing a member
    /// that is an event. (E.g. "System.Web.Forms.Button")</param>
    /// <param name="nameOfEvent">The name of the member in <paramref name="objectTypeName"/>
    /// that is the event (E.g. "Clicked")</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>The display name of the method, and a unique to for the method.</returns>
    public static IEnumerable<Tuple<string, string>> GetCompatibleEventHandlers(
        Document document, string className, string objectTypeName, string nameOfEvent, CancellationToken cancellationToken)
    {
        var compilation = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
        var type = compilation.GetTypeByMetadataName(className);
        if (type == null)
        {
            throw new InvalidOperationException();
        }
 
        var eventMember = GetEventSymbol(document, objectTypeName, nameOfEvent, type, cancellationToken);
        if (eventMember == null)
        {
            throw new InvalidOperationException();
        }
 
        var eventType = ((IEventSymbol)eventMember).Type;
        if (eventType.Kind != SymbolKind.NamedType)
        {
            throw new InvalidOperationException(ServicesVSResources.Event_type_is_invalid);
        }
 
        var methods = type.GetMembers().OfType<IMethodSymbol>().Where(m => m.CompatibleSignatureToDelegate((INamedTypeSymbol)eventType));
        return methods.Select(m => Tuple.Create(m.Name, ConstructMemberId(m)));
    }
 
    public static string GetEventHandlerMemberId(Document document, string className, string objectTypeName, string nameOfEvent, string eventHandlerName, CancellationToken cancellationToken)
    {
        var nameAndId = GetCompatibleEventHandlers(document, className, objectTypeName, nameOfEvent, cancellationToken).SingleOrDefault(pair => pair.Item1 == eventHandlerName);
        return nameAndId?.Item2;
    }
 
    /// <summary>
    /// Ensure that an event handler exists for a given event.
    /// </summary>
    /// <param name="thisDocument">The document corresponding to this operation.</param>
    /// <param name="targetDocument">The document to generate the event handler in if it doesn't
    /// exist.</param>
    /// <param name="className">The name of the type to generate the event handler in.</param>
    /// <param name="objectName">The name of the event member (if <paramref
    /// name="useHandlesClause"/> is true)</param>
    /// <param name="objectTypeName">The name of the type containing the event.</param>
    /// <param name="nameOfEvent">The name of the event member in <paramref
    /// name="objectTypeName"/></param>
    /// <param name="eventHandlerName">The name of the method to be hooked up to the
    /// event.</param>
    /// <param name="itemidInsertionPoint">The VS itemid of the file to generate the event
    /// handler in.</param>
    /// <param name="useHandlesClause">If true, a vb "Handles" clause will be generated for the
    /// handler.</param>
    /// <param name="additionalFormattingRule">An additional formatting rule that can be used to
    /// format the newly inserted method</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>Either the unique id of the method if it already exists, or the unique id of
    /// the to be generated method, the text of the to be generated method, and the position in
    /// <paramref name="itemidInsertionPoint"/> where the text should be inserted.</returns>
#pragma warning disable IDE0060 // Remove unused parameter - API used by partner (Venus), avoid build break.
    public static Tuple<string, string, VsTextSpan> EnsureEventHandler(
        Document thisDocument,
        Document targetDocument,
        string className,
        string objectName,
        string objectTypeName,
        string nameOfEvent,
        string eventHandlerName,
        uint itemidInsertionPoint,
        bool useHandlesClause,
        AbstractFormattingRule additionalFormattingRule,
        CancellationToken cancellationToken)
#pragma warning restore IDE0060 // Remove unused parameter
    {
        var thisCompilation = thisDocument.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
        var type = thisCompilation.GetTypeByMetadataName(className);
 
        var existingEventHandlers = GetCompatibleEventHandlers(targetDocument, className, objectTypeName, nameOfEvent, cancellationToken);
        var existingHandler = existingEventHandlers.SingleOrDefault(e => e.Item1 == eventHandlerName);
        if (existingHandler != null)
        {
            return Tuple.Create(existingHandler.Item2, (string)null, default(VsTextSpan));
        }
 
        // Okay, it doesn't exist yet.  Let's create it.
        var codeGenerationService = targetDocument.GetLanguageService<ICodeGenerationService>();
        var syntaxFactory = targetDocument.GetLanguageService<SyntaxGenerator>();
        var eventMember = GetEventSymbol(thisDocument, objectTypeName, nameOfEvent, type, cancellationToken);
        if (eventMember == null)
        {
            throw new InvalidOperationException();
        }
 
        var eventType = ((IEventSymbol)eventMember).Type;
        if (eventType.Kind != SymbolKind.NamedType || ((INamedTypeSymbol)eventType).DelegateInvokeMethod == null)
        {
            throw new InvalidOperationException(ServicesVSResources.Event_type_is_invalid);
        }
 
        var handlesExpressions = useHandlesClause
            ? ImmutableArray.Create(syntaxFactory.MemberAccessExpression(
                    objectName != null ? syntaxFactory.IdentifierName(objectName) : syntaxFactory.ThisExpression(),
                    syntaxFactory.IdentifierName(nameOfEvent)))
            : default;
 
        var invokeMethod = ((INamedTypeSymbol)eventType).DelegateInvokeMethod;
        var newMethod = CodeGenerationSymbolFactory.CreateMethodSymbol(
            attributes: default,
            accessibility: Accessibility.Protected,
            modifiers: new DeclarationModifiers(),
            returnType: targetDocument.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetSpecialType(SpecialType.System_Void),
            refKind: RefKind.None,
            explicitInterfaceImplementations: default,
            name: eventHandlerName,
            typeParameters: default,
            parameters: invokeMethod.Parameters,
            statements: default,
            handlesExpressions: handlesExpressions);
 
        var annotation = new SyntaxAnnotation();
        newMethod = annotation.AddAnnotationToSymbol(newMethod);
        var codeModel = targetDocument.Project.Services.GetRequiredService<ICodeModelNavigationPointService>();
        var syntaxFacts = targetDocument.Project.Services.GetRequiredService<ISyntaxFactsService>();
 
        var targetSyntaxTree = targetDocument.GetSyntaxTreeSynchronously(cancellationToken);
 
        var position = type.Locations.First(loc => loc.SourceTree == targetSyntaxTree).SourceSpan.Start;
        var destinationType = syntaxFacts.GetContainingTypeDeclaration(targetSyntaxTree.GetRoot(cancellationToken), position);
        var documentOptions = targetDocument.GetLineFormattingOptionsAsync(cancellationToken).AsTask().WaitAndGetResult_Venus(cancellationToken);
        var insertionPoint = codeModel.GetEndPoint(destinationType, documentOptions, EnvDTE.vsCMPart.vsCMPartBody);
 
        if (insertionPoint == null)
        {
            throw new InvalidOperationException(ServicesVSResources.Can_t_find_where_to_insert_member);
        }
 
        var options = targetDocument.GetCleanCodeGenerationOptionsAsync(cancellationToken).AsTask().WaitAndGetResult_Venus(cancellationToken);
 
        var info = codeGenerationService.GetInfo(new CodeGenerationContext(autoInsertionLocation: false), options.GenerationOptions, destinationType.SyntaxTree.Options);
        var newType = codeGenerationService.AddMethod(destinationType, newMethod, info, cancellationToken);
        var newRoot = targetSyntaxTree.GetRoot(cancellationToken).ReplaceNode(destinationType, newType);
 
        newRoot = Simplifier.ReduceAsync(
            targetDocument.WithSyntaxRoot(newRoot), Simplifier.Annotation, options.CleanupOptions.SimplifierOptions, cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetSyntaxRootSynchronously(cancellationToken);
 
        newRoot = Formatter.Format(
            newRoot,
            Formatter.Annotation,
            targetDocument.Project.Solution.Services,
            options.CleanupOptions.FormattingOptions,
            [additionalFormattingRule, .. Formatter.GetDefaultFormattingRules(targetDocument)],
            cancellationToken);
 
        var newMember = newRoot.GetAnnotatedNodesAndTokens(annotation).Single();
        var newMemberText = newMember.ToFullString();
 
        // In VB, the final newline is likely a statement terminator in the parent - just add
        // one on so that things don't get messed.
        if (!newMemberText.EndsWith(Environment.NewLine, StringComparison.Ordinal))
        {
            newMemberText += Environment.NewLine;
        }
 
        return Tuple.Create(ConstructMemberId(newMethod), newMemberText, insertionPoint.Value.ToVsTextSpan());
    }
 
    public static bool TryGetMemberNavigationPoint(
        Document thisDocument,
        string className,
        string uniqueMemberID,
        out VsTextSpan textSpan,
        out Document targetDocument,
        CancellationToken cancellationToken)
    {
        targetDocument = null;
        textSpan = default;
 
        var type = thisDocument.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetTypeByMetadataName(className);
        var member = LookupMemberId(type, uniqueMemberID);
 
        if (member == null)
        {
            return false;
        }
 
        var codeModel = thisDocument.Project.Services.GetService<ICodeModelNavigationPointService>();
        var memberNode = member.DeclaringSyntaxReferences.Select(r => r.GetSyntax(cancellationToken)).FirstOrDefault();
        if (memberNode != null)
        {
            var memberNodeDocument = thisDocument.Project.Solution.GetDocument(memberNode.SyntaxTree);
            var options = memberNodeDocument.GetLineFormattingOptionsAsync(cancellationToken).AsTask().WaitAndGetResult_Venus(cancellationToken);
            var navigationPoint = codeModel.GetStartPoint(memberNode, options, EnvDTE.vsCMPart.vsCMPartNavigate);
            if (navigationPoint != null)
            {
                targetDocument = memberNodeDocument;
                textSpan = navigationPoint.Value.ToVsTextSpan();
                return true;
            }
        }
 
        return false;
    }
 
    /// <summary>
    /// Get the display names and unique ids of all the members of the given type in <paramref
    /// name="className"/>.
    /// </summary>
    public static IEnumerable<Tuple<string, string>> GetMembers(
        Document document, string className, CODEMEMBERTYPE codeMemberType, CancellationToken cancellationToken)
    {
        var type = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetTypeByMetadataName(className);
 
        var compilation = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
        var semanticModel = document.GetSemanticModelAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
 
        var allMembers = codeMemberType == CODEMEMBERTYPE.CODEMEMBERTYPE_EVENTS
            ? semanticModel.LookupSymbols(position: type.Locations[0].SourceSpan.Start, container: type, name: null)
            : type.GetMembers();
 
        var members = allMembers.Where(m => IncludeMember(m, codeMemberType, compilation));
        return members.Select(m => Tuple.Create(m.Name, ConstructMemberId(m)));
    }
 
    /// <summary>
    /// Try to do a symbolic rename the specified symbol.
    /// </summary>
    /// <returns>False ONLY if it can't resolve the name.  Other errors result in the normal
    /// exception being propagated.</returns>
    public static bool TryRenameElement(
        Document document,
        ContainedLanguageRenameType clrt,
        string oldFullyQualifiedName,
        string newFullyQualifiedName,
        IEnumerable<IRefactorNotifyService> refactorNotifyServices,
        CancellationToken cancellationToken)
    {
        var symbol = FindSymbol(document, clrt, oldFullyQualifiedName, cancellationToken);
        if (symbol == null)
        {
            return false;
        }
 
        if (CodeAnalysis.Workspace.TryGetWorkspace(document.GetTextSynchronously(cancellationToken).Container, out var workspace))
        {
            var newName = newFullyQualifiedName[(newFullyQualifiedName.LastIndexOf('.') + 1)..];
            var options = new SymbolRenameOptions();
            var newSolution = Renamer.RenameSymbolAsync(document.Project.Solution, symbol, options, newName, cancellationToken).WaitAndGetResult_Venus(cancellationToken);
            var changedDocuments = newSolution.GetChangedDocuments(document.Project.Solution);
 
            var undoTitle = string.Format(EditorFeaturesResources.Rename_0_to_1, symbol.Name, newName);
            using (var workspaceUndoTransaction = workspace.OpenGlobalUndoTransaction(undoTitle))
            {
                // Notify third parties about the coming rename operation on the workspace, and let
                // any exceptions propagate through
                refactorNotifyServices.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure: true);
 
                if (!workspace.TryApplyChanges(newSolution))
                {
                    Exceptions.ThrowEFail();
                }
 
                // Notify third parties about the completed rename operation on the workspace, and
                // let any exceptions propagate through
                refactorNotifyServices.TryOnAfterGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure: true);
 
                workspaceUndoTransaction.Commit();
            }
 
            RenameTrackingDismisser.DismissRenameTracking(workspace, changedDocuments);
            return true;
        }
        else
        {
            return false;
        }
    }
 
    private static bool IncludeMember(ISymbol member, CODEMEMBERTYPE memberType, Compilation compilation)
    {
        if (!member.CanBeReferencedByName)
        {
            return false;
        }
 
        switch (memberType)
        {
            case CODEMEMBERTYPE.CODEMEMBERTYPE_EVENT_HANDLERS:
                // NOTE: the Dev10 C# codebase just returned 
                if (member.Kind != SymbolKind.Method)
                {
                    return false;
                }
 
                var method = (IMethodSymbol)member;
                if (!method.ReturnsVoid)
                {
                    return false;
                }
 
                if (method.Parameters.Length != 2)
                {
                    return false;
                }
 
                if (!method.Parameters[0].Type.Equals(compilation.ObjectType))
                {
                    return false;
                }
 
                if (!method.Parameters[1].Type.InheritsFromOrEquals(compilation.EventArgsType()))
                {
                    return false;
                }
 
                return true;
 
            case CODEMEMBERTYPE.CODEMEMBERTYPE_EVENTS:
                return member.Kind == SymbolKind.Event;
 
            case CODEMEMBERTYPE.CODEMEMBERTYPE_USER_FUNCTIONS:
                return member.Kind == SymbolKind.Method;
 
            default:
                throw new ArgumentException("InvalidValue", nameof(memberType));
        }
    }
 
    private static ISymbol FindSymbol(
        Document document, ContainedLanguageRenameType renameType, string fullyQualifiedName, CancellationToken cancellationToken)
    {
        switch (renameType)
        {
            case ContainedLanguageRenameType.CLRT_CLASS:
                return document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetTypeByMetadataName(fullyQualifiedName);
 
            case ContainedLanguageRenameType.CLRT_CLASSMEMBER:
                var lastDot = fullyQualifiedName.LastIndexOf('.');
                var typeName = fullyQualifiedName[..lastDot];
                var memberName = fullyQualifiedName.Substring(lastDot + 1, fullyQualifiedName.Length - lastDot - 1);
                var type = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GetTypeByMetadataName(typeName);
                var semanticModel = document.GetSemanticModelAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
                var membersOfName = type.GetMembers(memberName);
                return membersOfName.SingleOrDefault();
 
            case ContainedLanguageRenameType.CLRT_NAMESPACE:
                var ns = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken).GlobalNamespace;
                var parts = fullyQualifiedName.Split('.');
                for (var i = 0; i < parts.Length && ns != null; i++)
                {
                    ns = ns.GetNamespaceMembers().SingleOrDefault(n => n.Name == parts[i]);
                }
 
                return ns;
 
            case ContainedLanguageRenameType.CLRT_OTHER:
                throw new NotSupportedException(ServicesVSResources.Can_t_rename_other_elements);
 
            default:
                throw new InvalidOperationException(ServicesVSResources.Unknown_rename_type);
        }
    }
 
    internal static string ConstructMemberId(ISymbol member)
    {
        if (member.Kind == SymbolKind.Method)
        {
            return string.Format("{0}({1})", member.Name, string.Join(",", ((IMethodSymbol)member).Parameters.Select(p => p.Type.ToDisplayString())));
        }
        else if (member.Kind == SymbolKind.Event)
        {
            return member.Name + "(EVENT)";
        }
        else
        {
            throw new NotSupportedException(ServicesVSResources.IDs_are_not_supported_for_this_symbol_type);
        }
    }
 
    internal static ISymbol LookupMemberId(INamedTypeSymbol type, string uniqueMemberID)
    {
        var memberName = uniqueMemberID[..uniqueMemberID.IndexOf('(')];
        var members = type.GetMembers(memberName).Where(m => m.Kind == SymbolKind.Method);
 
        foreach (var m in members)
        {
            if (ConstructMemberId(m) == uniqueMemberID)
            {
                return m;
            }
        }
 
        return null;
    }
 
    private static ISymbol GetEventSymbol(
        Document document, string objectTypeName, string nameOfEvent, INamedTypeSymbol type, CancellationToken cancellationToken)
    {
        var compilation = document.Project.GetCompilationAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
        var semanticModel = document.GetSemanticModelAsync(cancellationToken).WaitAndGetResult_Venus(cancellationToken);
 
        var objectType = compilation.GetTypeByMetadataName(objectTypeName);
        if (objectType == null)
        {
            throw new InvalidOperationException();
        }
 
        var containingTree = document.GetSyntaxTreeSynchronously(cancellationToken);
        var typeLocation = type.Locations.FirstOrDefault(d => d.SourceTree == containingTree);
        if (typeLocation == null)
        {
            throw new InvalidOperationException();
        }
 
        return semanticModel.LookupSymbols(typeLocation.SourceSpan.Start, objectType, nameOfEvent).SingleOrDefault(m => m.Kind == SymbolKind.Event);
    }
}