File: src\Analyzers\Core\CodeFixes\ImplementInterface\ImplementHelpers.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.Threading;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.ImplementInterface;
 
internal static class ImplementHelpers
{
    public static ImmutableArray<ISymbol> GetDelegatableMembers(
        Document document,
        INamedTypeSymbol namedType,
        Func<ITypeSymbol, bool> includeMemberType,
        CancellationToken cancellationToken)
    {
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
 
        var fields = namedType.GetMembers()
            .OfType<IFieldSymbol>()
            .Where(f => !f.IsImplicitlyDeclared)
            .Where(f => includeMemberType(f.Type))
            .ToImmutableArray();
 
        var properties = namedType.GetMembers()
            .OfType<IPropertySymbol>()
            .Where(p => !p.IsImplicitlyDeclared && p.Parameters.Length == 0 && p.GetMethod != null)
            .Where(p => includeMemberType(p.Type))
            .ToImmutableArray();
 
        var parameters = GetNonCapturedPrimaryConstructorParameters(fields, properties);
 
        return [.. fields, .. properties, .. parameters];
 
        ImmutableArray<IParameterSymbol> GetNonCapturedPrimaryConstructorParameters(
            ImmutableArray<IFieldSymbol> fields,
            ImmutableArray<IPropertySymbol> properties)
        {
            using var _2 = ArrayBuilder<IParameterSymbol>.GetInstance(out var result);
 
            var primaryConstructor = namedType.InstanceConstructors
                .FirstOrDefault(c => c.Parameters.Length > 0 && c.Parameters[0].IsPrimaryConstructor(cancellationToken));
            if (primaryConstructor != null)
            {
                foreach (var parameter in primaryConstructor.Parameters)
                {
                    if (includeMemberType(parameter.Type) &&
                        !IsAssignedToFieldOrProperty(fields, properties, parameter))
                    {
                        result.Add(parameter);
                    }
                }
            }
 
            return result.ToImmutableAndClear();
        }
 
        bool IsAssignedToFieldOrProperty(ImmutableArray<IFieldSymbol> fields, ImmutableArray<IPropertySymbol> properties, IParameterSymbol parameter)
            => fields.Any(f => IsAssignedToField(f, parameter)) || properties.Any(p => IsAssignedToProperty(p, parameter));
 
        bool IsAssignedToField(IFieldSymbol field, IParameterSymbol parameter)
        {
            if (field.DeclaringSyntaxReferences is [var syntaxRef, ..])
            {
                var declarator = syntaxRef.GetSyntax(cancellationToken);
                if (syntaxFacts.IsVariableDeclarator(declarator))
                {
                    var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(declarator);
                    if (InitializerReferencesParameter(initializer, parameter))
                        return true;
                }
            }
 
            return false;
        }
 
        bool IsAssignedToProperty(IPropertySymbol property, IParameterSymbol parameter)
        {
            if (property.DeclaringSyntaxReferences is [var syntaxRef, ..])
            {
                var declarator = syntaxRef.GetSyntax(cancellationToken);
                if (syntaxFacts.IsPropertyDeclaration(declarator))
                {
                    var initializer = syntaxFacts.GetInitializerOfPropertyDeclaration(declarator);
                    if (InitializerReferencesParameter(initializer, parameter))
                        return true;
                }
            }
 
            return false;
        }
 
        bool InitializerReferencesParameter(SyntaxNode? initializer, IParameterSymbol parameter)
        {
            if (initializer != null)
            {
                var value = syntaxFacts.GetValueOfEqualsValueClause(initializer);
                value = syntaxFacts.WalkDownParentheses(value);
 
                if (syntaxFacts.IsIdentifierName(value))
                {
                    syntaxFacts.GetNameAndArityOfSimpleName(value, out var name, out _);
                    if (name == parameter.Name)
                        return true;
                }
            }
 
            return false;
        }
    }
 
    public static bool IsLessAccessibleThan(ISymbol? first, INamedTypeSymbol second)
    {
        if (first is null)
            return false;
 
        if (first.DeclaredAccessibility <= Accessibility.NotApplicable ||
            second.DeclaredAccessibility <= Accessibility.NotApplicable)
        {
            return false;
        }
 
        if (first.DeclaredAccessibility < second.DeclaredAccessibility)
            return true;
 
        switch (first)
        {
            case IPropertySymbol propertySymbol:
                if (IsTypeLessAccessibleThanOtherType(propertySymbol.Type, second, []))
                    return true;
 
                if (IsLessAccessibleThan(propertySymbol.GetMethod, second))
                    return true;
 
                if (IsLessAccessibleThan(propertySymbol.SetMethod, second))
                    return true;
 
                return false;
 
            case IMethodSymbol methodSymbol:
                if (IsTypeLessAccessibleThanOtherType(methodSymbol.ReturnType, second, []))
                    return true;
 
                foreach (var parameter in methodSymbol.Parameters)
                {
                    if (IsTypeLessAccessibleThanOtherType(parameter.Type, second, []))
                        return true;
                }
 
                foreach (var typeArg in methodSymbol.TypeArguments)
                {
                    if (IsTypeLessAccessibleThanOtherType(typeArg, second, []))
                        return true;
                }
 
                return false;
 
            case IEventSymbol eventSymbol:
                return IsTypeLessAccessibleThanOtherType(eventSymbol.Type, second, []);
 
            default:
                return false;
        }
    }
 
    private static bool IsTypeLessAccessibleThanOtherType(ITypeSymbol? first, INamedTypeSymbol second, HashSet<ITypeSymbol> alreadyCheckingTypes)
    {
        if (first is null)
            return false;
 
        alreadyCheckingTypes.Add(first);
 
        if (first is ITypeParameterSymbol typeParameter)
        {
            foreach (var constraint in typeParameter.ConstraintTypes)
            {
                if (alreadyCheckingTypes.Contains(constraint))
                    continue;
 
                if (IsTypeLessAccessibleThanOtherType(constraint, second, alreadyCheckingTypes))
                    return true;
            }
        }
 
        if (first.DeclaredAccessibility <= Accessibility.NotApplicable ||
            second.DeclaredAccessibility <= Accessibility.NotApplicable)
        {
            return false;
        }
 
        if (first.DeclaredAccessibility < second.DeclaredAccessibility)
            return true;
 
        if (first is INamedTypeSymbol namedType)
        {
            foreach (var genericParam in namedType.TypeArguments)
            {
                if (alreadyCheckingTypes.Contains(genericParam))
                    continue;
 
                if (IsTypeLessAccessibleThanOtherType(genericParam, second, alreadyCheckingTypes))
                    return true;
            }
        }
 
        if (IsTypeLessAccessibleThanOtherType(first.ContainingType, second, alreadyCheckingTypes))
            return true;
 
        return false;
    }
 
    public static bool ShouldImplementDisposePattern(Compilation compilation, IImplementInterfaceInfo state, bool explicitly)
    {
        // Dispose pattern should be implemented only if -
        // 1. An interface named 'System.IDisposable' is unimplemented.
        // 2. This interface has one and only one member - a non-generic method named 'Dispose' that takes no arguments and returns 'void'.
        // 3. The implementing type is a class that does not already declare any conflicting members named
        //    'disposedValue' or 'Dispose' (because we will be generating a 'disposedValue' field and a couple of
        //    methods named 'Dispose' as part of implementing the dispose pattern).
        if (state.ClassOrStructType.TypeKind != TypeKind.Class)
            return false;
 
        var disposeMethod = TryGetIDisposableDispose(compilation);
        if (disposeMethod == null)
            return false;
 
        var idisposableType = disposeMethod.ContainingType;
        var unimplementedMembers = explicitly
            ? state.MembersWithoutExplicitImplementation
            : state.MembersWithoutExplicitOrImplicitImplementationWhichCanBeImplicitlyImplemented;
        if (!unimplementedMembers.Any(static (m, idisposableType) => m.type.Equals(idisposableType), idisposableType))
            return false;
 
        // The dispose pattern is only applicable if the implementing type does
        // not already have an implementation of IDisposableDispose.
        return state.ClassOrStructType.FindImplementationForInterfaceMember(disposeMethod) == null;
    }
 
    public static IMethodSymbol? TryGetIDisposableDispose(Compilation compilation)
    {
        // Get symbol for 'System.IDisposable'.
        var idisposable = compilation.GetSpecialType(SpecialType.System_IDisposable);
        if (idisposable?.TypeKind == TypeKind.Interface)
        {
            foreach (var member in idisposable.GetMembers(nameof(IDisposable.Dispose)))
            {
                if (member is IMethodSymbol { IsStatic: false, ReturnsVoid: true, Arity: 0, Parameters.Length: 0 } disposeMethod)
                    return disposeMethod;
            }
        }
 
        return null;
    }
}