| File: src\roslyn\src\Analyzers\CSharp\CodeFixes\ConvertToRecord\PositionalParameterInfo.cs | Web Access |
| Project: src\roslyn\src\CodeStyle\CSharp\CodeFixes\Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CSharp.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.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.ConvertToRecord; /// <summary> /// Represents a property that should be added as a positional parameter /// </summary> /// <param name="Declaration">Original declaration, null iff IsInherited is true /// Null iff <see cref="IsInherited"/> is true</param> /// <param name="Symbol">Symbol of the property</param> /// <param name="KeepAsOverride">Whether we should keep the original declaration present</param> internal sealed record PositionalParameterInfo( PropertyDeclarationSyntax? Declaration, IPropertySymbol Symbol, bool KeepAsOverride) { /// <summary> /// Whether this property is inherited from another base record /// </summary> [MemberNotNullWhen(false, nameof(Declaration))] public bool IsInherited => Declaration == null; public static ImmutableArray<PositionalParameterInfo> GetPropertiesForPositionalParameters( ImmutableArray<PropertyDeclarationSyntax> properties, INamedTypeSymbol type, SemanticModel semanticModel, CancellationToken cancellationToken) { using var _ = ArrayBuilder<PositionalParameterInfo>.GetInstance(out var resultBuilder); // get all declared property symbols, put inherited property symbols first var symbols = properties.SelectAsArray(p => semanticModel.GetRequiredDeclaredSymbol(p, cancellationToken)); // add inherited properties from a potential base record first var inheritedProperties = GetInheritedPositionalParams(type, cancellationToken); resultBuilder.AddRange(inheritedProperties.Select(property => new PositionalParameterInfo( Declaration: null, property, KeepAsOverride: false))); // The user may not know about init or be converting code from before init was introduced. // In this case we can convert set properties to init ones var allowSetToInitConversion = !symbols .Any(symbol => symbol.SetMethod is IMethodSymbol { IsInitOnly: true }); resultBuilder.AddRange(properties.Zip(symbols, (syntax, symbol) => ShouldConvertProperty(syntax, symbol, type) switch { ConvertStatus.DoNotConvert => null, ConvertStatus.Override => new PositionalParameterInfo(syntax, symbol, KeepAsOverride: true), ConvertStatus.OverrideIfConvertingSetToInit => new PositionalParameterInfo(syntax, symbol, !allowSetToInitConversion), ConvertStatus.AlwaysConvert => new PositionalParameterInfo(syntax, symbol, KeepAsOverride: false), _ => throw ExceptionUtilities.Unreachable(), }).WhereNotNull()); return resultBuilder.ToImmutableAndClear(); } public static ImmutableArray<IPropertySymbol> GetInheritedPositionalParams( INamedTypeSymbol currentType, CancellationToken cancellationToken) { var baseType = currentType.BaseType; if (baseType != null && baseType.TryGetPrimaryConstructor(out var basePrimary)) { return basePrimary.Parameters .Select(param => param.GetAssociatedSynthesizedRecordProperty(cancellationToken)) .WhereNotNull() .AsImmutable(); } return []; } /// <summary> /// for each property, say whether we can convert /// to primary constructor parameter or not (and whether it would imply changes) /// </summary> private enum ConvertStatus { /// <summary> /// no way we can convert this /// </summary> DoNotConvert, /// <summary> /// we can convert this because we feel it would be used in a primary constructor, /// but some accessibility is non-default and we want to override /// </summary> Override, /// <summary> /// we can convert this if we see that the user only ever uses set (not init) /// otherwise we should give an override /// </summary> OverrideIfConvertingSetToInit, /// <summary> /// we can convert this without changing the meaning /// </summary> AlwaysConvert } private static ConvertStatus ShouldConvertProperty( PropertyDeclarationSyntax property, IPropertySymbol propertySymbol, INamedTypeSymbol containingType) { // properties with identifiers or expression bodies are too complex to move. // unimplemented or static properties shouldn't be in a constructor. if (property.Initializer != null || property.ExpressionBody != null || propertySymbol.IsAbstract || propertySymbol.IsStatic) { return ConvertStatus.DoNotConvert; } // more restrictive than internal (protected, private, private protected, or unspecified (private by default)). // We allow internal props to be converted to public auto-generated ones // because it's still as accessible as a constructor would be from outside the class. if (propertySymbol.DeclaredAccessibility < Accessibility.Internal) { return ConvertStatus.DoNotConvert; } // no accessors declared if (property.AccessorList == null || property.AccessorList.Accessors.IsEmpty()) { return ConvertStatus.DoNotConvert; } // When we convert to primary constructor parameters, the auto-generated properties have default behavior // Here are the cases where we wouldn't substantially change default behavior // - No accessors can have any explicit implementation or modifiers // - This is because it would indicate complex functionality or explicit hiding which is not default // - class records and readonly struct records must have: // - public get accessor // - optionally a public init accessor // - note: if this is not provided the user can still initialize the property in the constructor, // so it's like init but without the user ability to initialize outside the constructor // - for non-readonly structs, we must have: // - public get accessor // - public set accessor // If the user has a private/protected set method, it could still make sense to be a primary constructor // but we should provide the override in case the user sets the property from within the class var getAccessor = propertySymbol.GetMethod; var setAccessor = propertySymbol.SetMethod; var accessors = property.AccessorList.Accessors; if (accessors.Any(a => a.Body != null || a.ExpressionBody != null) || getAccessor == null || // private get means they probably don't want it seen from the constructor getAccessor.DeclaredAccessibility < Accessibility.Internal) { return ConvertStatus.DoNotConvert; } // we consider a internal (by default) get on an internal property as public // but if the user specifically declares a more restrictive accessibility // it would indicate they want to keep it safer than the rest of the property // and we should respect that if (getAccessor.DeclaredAccessibility < propertySymbol.DeclaredAccessibility) { return ConvertStatus.Override; } if (containingType.TypeKind == TypeKind.Struct && !containingType.IsReadOnly) { // in a struct, our default is to have a public set // but anything else we can still convert and override if (setAccessor == null) { return ConvertStatus.Override; } // if the user had their property as internal then we are fine with completely moving // an internal (by default) set method, but if they explicitly mark the set as internal // while the property is public we want to keep that behavior if (setAccessor.DeclaredAccessibility != Accessibility.Public && setAccessor.DeclaredAccessibility != propertySymbol.DeclaredAccessibility) { return ConvertStatus.Override; } if (setAccessor.IsInitOnly) { return ConvertStatus.Override; } } else { // either we are a class or readonly struct, the default is no set or init only set if (setAccessor != null) { if (setAccessor.DeclaredAccessibility != Accessibility.Public && setAccessor.DeclaredAccessibility != propertySymbol.DeclaredAccessibility) { return ConvertStatus.Override; } if (!setAccessor.IsInitOnly) { return ConvertStatus.OverrideIfConvertingSetToInit; } } } return ConvertStatus.AlwaysConvert; } }