File: EditAndContinue\Utilities\Extensions.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.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.Contracts.EditAndContinue;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.EditAndContinue;
 
internal static partial class Extensions
{
    internal static LinePositionSpan AddLineDelta(this LinePositionSpan span, int lineDelta)
        => new(new LinePosition(span.Start.Line + lineDelta, span.Start.Character), new LinePosition(span.End.Line + lineDelta, span.End.Character));
 
    internal static SourceFileSpan AddLineDelta(this SourceFileSpan span, int lineDelta)
        => new(span.Path, span.Span.AddLineDelta(lineDelta));
 
    internal static int GetLineDelta(this LinePositionSpan oldSpan, LinePositionSpan newSpan)
        => newSpan.Start.Line - oldSpan.Start.Line;
 
    internal static bool Contains(this LinePositionSpan container, LinePositionSpan span)
        => span.Start >= container.Start && span.End <= container.End;
 
    public static LinePositionSpan ToLinePositionSpan(this SourceSpan span)
        => new(new(span.StartLine, span.StartColumn), new(span.EndLine, span.EndColumn));
 
    public static SourceSpan ToSourceSpan(this LinePositionSpan span)
        => new(span.Start.Line, span.Start.Character, span.End.Line, span.End.Character);
 
    /// <summary>
    /// True if the project supports Edit and Continue.
    /// Only depends on the language of the project and never changes.
    /// 
    /// Source generated files in the project must match the paths used by the compiler, otherwise
    /// different metadata might be emitted for file-scoped classes between compilation and EnC.
    /// </summary>
    public static bool SupportsEditAndContinue(this Project project, TraceLog? log = null)
    {
        if (project.FilePath == null)
        {
            log?.Write("Project '{0}' (id '{1}') doesn't support EnC: no file path", project.Name, project.Id);
            return false;
        }
 
        if (project.Services.GetService<IEditAndContinueAnalyzer>() == null)
        {
            log?.Write("Project '{0}' doesn't support EnC: no EnC service", project.FilePath);
            return false;
        }
 
        if (!project.CompilationOutputInfo.HasEffectiveGeneratedFilesOutputDirectory)
        {
            log?.Write("Project '{0}' doesn't support EnC: no generated files output directory", project.FilePath);
            return false;
        }
 
        return true;
    }
 
    public static bool SupportsEditAndContinue(this TextDocumentState textDocumentState)
    {
        if (textDocumentState.Attributes.DesignTimeOnly)
        {
            return false;
        }
 
        if (!PathUtilities.IsAbsolute(textDocumentState.FilePath))
        {
            return false;
        }
 
        if (textDocumentState is DocumentState documentState)
        {
            if (!documentState.SupportsSyntaxTree)
            {
                return false;
            }
 
            // WPF design time documents are added to the Workspace by the Project System as regular documents,
            // although they are not compiled into the binary.
            if (IsWpfDesignTimeOnlyDocument(textDocumentState.FilePath, documentState.LanguageServices.Language))
            {
                return false;
            }
 
            // Razor generated documents are added to the Workspace by the Web Tools editor but aren't used at runtime,
            // so don't need to be considered for edit and continue.
            if (IsRazorDesignTimeOnlyDocument(textDocumentState.FilePath))
            {
                return false;
            }
        }
 
        return true;
    }
 
    private static bool IsWpfDesignTimeOnlyDocument(string filePath, string language)
        => language switch
        {
            LanguageNames.CSharp => filePath.EndsWith(".g.i.cs", StringComparison.OrdinalIgnoreCase),
            LanguageNames.VisualBasic => filePath.EndsWith(".g.i.vb", StringComparison.OrdinalIgnoreCase),
            _ => false
        };
 
    private static bool IsRazorDesignTimeOnlyDocument(string filePath)
        => filePath.EndsWith(".razor.g.cs", StringComparison.OrdinalIgnoreCase) ||
            filePath.EndsWith(".cshtml.g.cs", StringComparison.OrdinalIgnoreCase);
 
    public static ManagedHotReloadDiagnostic ToHotReloadDiagnostic(this DiagnosticData data, ModuleUpdateStatus updateStatus, bool isRudeEdit)
    {
        var fileSpan = data.DataLocation.MappedFileSpan;
 
        return new(
            data.Id,
            data.Message ?? FeaturesResources.Unknown_error_occurred,
            isRudeEdit
                ? data.DefaultSeverity switch
                {
                    DiagnosticSeverity.Error => ManagedHotReloadDiagnosticSeverity.RestartRequired,
                    DiagnosticSeverity.Warning => ManagedHotReloadDiagnosticSeverity.Warning,
                    _ => throw ExceptionUtilities.UnexpectedValue(data.DefaultSeverity)
                }
                : updateStatus == ModuleUpdateStatus.RestartRequired
                    ? ManagedHotReloadDiagnosticSeverity.RestartRequired
                    : (data.Severity == DiagnosticSeverity.Error)
                        ? ManagedHotReloadDiagnosticSeverity.Error
                        : ManagedHotReloadDiagnosticSeverity.Warning,
            fileSpan.Path ?? "",
            fileSpan.Span.ToSourceSpan());
    }
 
    public static bool IsSynthesized(this ISymbol symbol)
        => symbol.IsImplicitlyDeclared || symbol.IsSynthesizedAutoProperty() || symbol.IsSynthesizedParameter();
 
    public static bool IsSynthesizedAutoProperty(this IPropertySymbol property)
        => property is { GetMethod.IsImplicitlyDeclared: true, SetMethod.IsImplicitlyDeclared: true };
 
    public static bool IsSynthesizedAutoProperty(this ISymbol symbol)
        => symbol is IPropertySymbol property && property.IsSynthesizedAutoProperty();
 
    public static bool IsSynthesizedParameter(this ISymbol symbol)
        => symbol is IParameterSymbol parameter && parameter.IsSynthesizedParameter();
 
    /// <summary>
    /// True if the parameter is synthesized based on some other symbol (origin).
    /// In some cases <see cref="ISymbol.IsImplicitlyDeclared"/> of parameters of synthezied methods might be false.
    /// The parameter syntax in these cases is associated with multiple symbols.
    /// We pick one that is considered the origin and the others are considered synthesized based on it.
    /// 
    /// 1) Parameter of a record deconstructor
    ///    Considered synthesized since the primary parameter syntax represents the parameter of the primary constructor.
    ///    The deconstructor is synthesized based on the primary constructor.
    /// 2) Parameter of an Invoke method of a delegate type
    ///    The Invoke method is synthesized but its parameters represent the parameters of the delegate.
    ///    The parameters of BeginInvoke and EndInvoke are synthesized based on the Invoke method parameters.
    /// </summary>
    public static bool IsSynthesizedParameter(this IParameterSymbol parameter)
        => parameter.IsImplicitlyDeclared || parameter.ContainingSymbol.IsSynthesized() && parameter.ContainingSymbol != parameter.ContainingType.DelegateInvokeMethod;
 
    public static bool IsAutoProperty(this ISymbol symbol)
        => symbol is IPropertySymbol property && IsAutoProperty(property);
 
    public static bool IsAutoProperty(this IPropertySymbol property)
        => property.ContainingType.GetMembers().Any(static (member, property) => member is IFieldSymbol field && field.AssociatedSymbol == property, property);
 
    public static bool HasSynthesizedDefaultConstructor(this INamedTypeSymbol type)
        => !type.InstanceConstructors.Any(static c => !(c.Parameters is [] || c.ContainingType.IsRecord && c.IsCopyConstructor()));
 
    public static bool IsCopyConstructor(this ISymbol symbol)
        => symbol is IMethodSymbol { Parameters: [var parameter] } && SymbolEqualityComparer.Default.Equals(parameter.Type, symbol.ContainingType);
 
    public static bool HasDeconstructorSignature(this IMethodSymbol method, IMethodSymbol constructor)
        => method.Parameters.Length > 0 &&
           method.Parameters.Length == constructor.Parameters.Length &&
           method.Parameters.All(
               static (param, constructor) => param.RefKind == RefKind.Out && param.Type.Equals(constructor.Parameters[param.Ordinal].Type, SymbolEqualityComparer.Default),
               constructor);
 
    // TODO: use AssociatedSymbol to tie field to the parameter (see https://github.com/dotnet/roslyn/issues/69115)
    public static IFieldSymbol? GetPrimaryParameterBackingField(this IParameterSymbol parameter)
        => (IFieldSymbol?)parameter.ContainingType.GetMembers().FirstOrDefault(
            static (member, parameter) => member is IFieldSymbol field && ParsePrimaryParameterBackingFieldName(field.Name, out var paramName) && paramName == parameter.Name, parameter);
 
    private static bool ParsePrimaryParameterBackingFieldName(string fieldName, [NotNullWhen(true)] out string? parameterName)
    {
        int closing;
        if (fieldName.StartsWith("<") && (closing = fieldName.IndexOf(">P")) > 1)
        {
            parameterName = fieldName.Substring(1, closing - 1);
            return true;
        }
 
        parameterName = null;
        return false;
    }
 
    /// <summary>
    /// Returns a deconstructor that matches the parameters of the given <paramref name="constructor"/>, or null if there is none.
    /// </summary>
    public static IMethodSymbol? GetMatchingDeconstructor(this IMethodSymbol constructor)
        => (IMethodSymbol?)constructor.ContainingType.GetMembers(WellKnownMemberNames.DeconstructMethodName).FirstOrDefault(
            static (symbol, constructor) => symbol is IMethodSymbol method && HasDeconstructorSignature(method, constructor), constructor)?.PartialAsImplementation();
 
    /// <summary>
    /// Returns a partial implementation part of a partial member, or the member itself if it's not partial.
    /// </summary>
    public static ISymbol PartialAsImplementation(this ISymbol symbol)
        => PartialImplementationPart(symbol) ?? symbol;
 
    public static bool IsPartialDefinition(this ISymbol symbol)
        => symbol is IMethodSymbol { IsPartialDefinition: true } or IPropertySymbol { IsPartialDefinition: true };
 
    public static bool IsPartialImplementation(this ISymbol symbol)
        => symbol is IMethodSymbol { PartialDefinitionPart: not null } or IPropertySymbol { PartialDefinitionPart: not null };
 
    public static ISymbol? PartialDefinitionPart(this ISymbol symbol)
        => symbol switch
        {
            IMethodSymbol { PartialDefinitionPart: var def } => def,
            IPropertySymbol { PartialDefinitionPart: var def } => def,
            _ => null
        };
 
    public static ISymbol? PartialImplementationPart(this ISymbol symbol)
        => symbol switch
        {
            IMethodSymbol { PartialImplementationPart: var impl } => impl,
            IPropertySymbol { PartialImplementationPart: var impl } => impl,
            _ => null
        };
 
    /// <summary>
    /// Returns true if any member of the type implements an interface member explicitly.
    /// </summary>
    public static bool HasExplicitlyImplementedInterfaceMember(this INamedTypeSymbol type)
        => type.GetMembers().Any(static member => member.ExplicitInterfaceImplementations().Any());
}