/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Features/CSharp/Portable/InitializeParameter/CSharpInitializeMemberFromPrimaryConstructorParameterCodeRefactoringProvider.cs
308 строк
16 KB
Cyrus Najmabadi
Allow user to still create a new field/prop when offering to initialize an existing prop
14 июл 2025, 22:30
14 июл 2025, 22:30
2d85833
Код
Авторство
О чём код?
// 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.Composition; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeGeneration; using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.InitializeParameter; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Naming; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.InitializeParameter; using static InitializeParameterHelpers; using static InitializeParameterHelpersCore; using static SyntaxFactory; [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.InitializeMemberFromPrimaryConstructorParameter), Shared] [method: ImportingConstructor] [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] internal sealed partial class CSharpInitializeMemberFromPrimaryConstructorParameterCodeRefactoringProvider() : CodeRefactoringProvider { public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { var (document, _, cancellationToken) = context; var selectedParameter = await context.TryGetRelevantNodeAsync<ParameterSyntax>().ConfigureAwait(false); if (selectedParameter == null) return; if (selectedParameter.Parent is not ParameterListSyntax { Parent: TypeDeclarationSyntax(kind: SyntaxKind.ClassDeclaration or SyntaxKind.StructDeclaration) typeDeclaration }) return; var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var parameter = semanticModel.GetRequiredDeclaredSymbol(selectedParameter, cancellationToken); if (parameter?.Name is null or "") return; if (parameter.ContainingSymbol is not IMethodSymbol { MethodKind: MethodKind.Constructor } constructor) return; // See if we're already assigning this parameter to a field/property in this type. If so, there's nothing // more for us to do. var compilation = semanticModel.Compilation; var (initializerValue, _) = TryFindFieldOrPropertyInitializerValue(compilation, parameter, cancellationToken); if (initializerValue != null) return; // Haven't initialized any fields/properties with this parameter. Offer to assign to an existing matching // field/prop if we can find one, or add a new field/prop if we can't. var rules = await document.GetNamingRulesAsync(cancellationToken).ConfigureAwait(false); var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules); if (parameterNameParts.BaseName == "") return; var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false); var fieldOrProperty = TryFindMatchingUninitializedFieldOrPropertySymbol(); using var refactorings = TemporaryArray<CodeAction>.Empty; if (fieldOrProperty != null) { // Found a field/property that this parameter should be assigned to. Just offer the simple assignment to it. refactorings.Add(CreateCodeAction( string.Format(fieldOrProperty.Kind == SymbolKind.Field ? FeaturesResources.Initialize_field_0 : FeaturesResources.Initialize_property_0, fieldOrProperty.Name), cancellationToken => AddAssignmentForPrimaryConstructorAsync(document, parameter, fieldOrProperty, cancellationToken))); } AddCreateFieldOrPropertyCodeActions(); context.RegisterRefactorings(refactorings.ToImmutableAndClear(), context.Span); return; ISymbol? TryFindMatchingUninitializedFieldOrPropertySymbol() { // Look for a field/property that really looks like it corresponds to this parameter. Use a variety of // heuristics around the name/type to see if this is a match. var parameterWords = parameterNameParts.BaseNameParts; var containingType = parameter.ContainingType; var initializeParameterService = document.GetRequiredLanguageService<IInitializeParameterService>(); // Walk through the naming rules against this parameter's name to see what name the user would like for // it as a member in this type. Note that we have some fallback rules that use the standard conventions // around properties /fields so that can still find things even if the user has no naming preferences // set. foreach (var rule in rules) { var memberName = rule.NamingStyle.CreateName(parameterWords); foreach (var memberWithName in containingType.GetMembers(memberName)) { // We found members in our type with that name. If it's a writable field that we could assign // this parameter to, and it's not already been assigned to, then this field is a good candidate // for us to hook up to. if (memberWithName is IFieldSymbol { IsConst: false, DeclaringSyntaxReferences: [var syntaxRef1, ..] } field && IsImplicitConversion(compilation, source: parameter.Type, destination: field.Type) && syntaxRef1.GetSyntax(cancellationToken) is VariableDeclaratorSyntax { Initializer: null }) { return field; } // If it's a writable property that we could assign this parameter to, and it's not already been // assigned to, then this property is a good candidate for us to hook up to. if (memberWithName is IPropertySymbol { DeclaringSyntaxReferences: [var syntaxRef2, ..] } property && IsImplicitConversion(compilation, source: parameter.Type, destination: property.Type) && syntaxRef2.GetSyntax(cancellationToken) is PropertyDeclarationSyntax { Initializer: null }) { // We also allow assigning into a property of the form `=> throw new // NotImplementedException()`. That way users can easily spit out those methods, but then // convert them to be normal properties with ease. if (initializeParameterService.IsThrowNotImplementedProperty(compilation, property, cancellationToken)) return property; if (property.IsWritableInConstructor()) return property; } } } // Couldn't find any existing member. Just return nothing so we can offer to create a member for them. return null; } static CodeAction CreateCodeAction(string title, Func<CancellationToken, Task<Solution>> createSolution) => CodeAction.Create(title, createSolution, title); void AddCreateFieldOrPropertyCodeActions() { // Didn't find a field/prop that this parameter could be assigned to. Offer to create new one and assign to that. // Check if the surrounding parameters are assigned to another field in this class. If so, offer to make // this parameter into a field as well. Otherwise, default to generating a property var siblingFieldOrProperty = TryFindSiblingFieldOrProperty(); var field = CreateField(parameter); var property = CreateProperty(parameter); var fieldAction = CreateCodeAction( string.Format(FeaturesResources.Create_and_assign_field_0, field.Name), cancellationToken => AddMultipleMembersAsync(document, typeDeclaration, [parameter], [field], cancellationToken)); var propertyAction = CreateCodeAction( string.Format(FeaturesResources.Create_and_assign_property_0, property.Name), cancellationToken => AddMultipleMembersAsync(document, typeDeclaration, [parameter], [property], cancellationToken)); refactorings.Add(siblingFieldOrProperty is IFieldSymbol ? fieldAction : propertyAction); refactorings.Add(siblingFieldOrProperty is IFieldSymbol ? propertyAction : fieldAction); var parameters = GetParametersWithoutAssociatedMembers(); if (parameters.Length >= 2) { var allFieldsAction = CodeAction.Create( FeaturesResources.Create_and_assign_remaining_as_fields, cancellationToken => AddMultipleMembersAsync(document, typeDeclaration, parameters, parameters.SelectAsArray(CreateField), cancellationToken)); var allPropertiesAction = CodeAction.Create( FeaturesResources.Create_and_assign_remaining_as_properties, cancellationToken => AddMultipleMembersAsync(document, typeDeclaration, parameters, parameters.SelectAsArray(CreateProperty), cancellationToken)); refactorings.Add(siblingFieldOrProperty is IFieldSymbol ? allFieldsAction : allPropertiesAction); refactorings.Add(siblingFieldOrProperty is IFieldSymbol ? allPropertiesAction : allFieldsAction); } } ISymbol? TryFindSiblingFieldOrProperty() { foreach (var (siblingParam, _) in InitializeParameterHelpersCore.GetSiblingParameters(parameter)) { var (_, sibling) = TryFindFieldOrPropertyInitializerValue(compilation, siblingParam, cancellationToken); if (sibling != null) return sibling; } return null; } ImmutableArray<IParameterSymbol> GetParametersWithoutAssociatedMembers() { using var result = TemporaryArray<IParameterSymbol>.Empty; foreach (var parameter in constructor.Parameters) { var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules); if (parameterNameParts.BaseName == "") continue; var (assignmentOp, _) = TryFindFieldOrPropertyInitializerValue(compilation, parameter, cancellationToken); if (assignmentOp != null) continue; result.Add(parameter); } return result.ToImmutableAndClear(); } ISymbol CreateField(IParameterSymbol parameter) { var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules).BaseNameParts; var accessibilityLevel = formattingOptions.AccessibilityModifiersRequired is AccessibilityModifiersRequired.Never or AccessibilityModifiersRequired.OmitIfDefault ? Accessibility.NotApplicable : Accessibility.Private; foreach (var rule in rules) { if (rule.SymbolSpecification.AppliesTo(SymbolKind.Field, Accessibility.Private)) { return CodeGenerationSymbolFactory.CreateFieldSymbol( attributes: default, accessibilityLevel, DeclarationModifiers.ReadOnly, parameter.Type, name: GenerateUniqueName(parameter, parameterNameParts, rule), initializer: IdentifierName(parameter.Name.EscapeIdentifier())); } } // We place a special rule in s_builtInRules that matches all fields. So we should // always find a matching rule. throw ExceptionUtilities.Unreachable(); } ISymbol CreateProperty(IParameterSymbol parameter) { var parameterNameParts = IdentifierNameParts.CreateIdentifierNameParts(parameter, rules).BaseNameParts; foreach (var rule in rules) { if (rule.SymbolSpecification.AppliesTo(SymbolKind.Property, Accessibility.Public)) { return CodeGenerationSymbolFactory.CreatePropertySymbol( containingType: null, attributes: default, Accessibility.Public, modifiers: default, parameter.Type, RefKind.None, explicitInterfaceImplementations: default, name: GenerateUniqueName(parameter, parameterNameParts, rule), parameters: default, getMethod: CodeGenerationSymbolFactory.CreateAccessorSymbol( attributes: default, Accessibility.Public, statements: default), setMethod: null, initializer: IdentifierName(parameter.Name.EscapeIdentifier())); } } // We place a special rule in s_builtInRules that matches all properties. So we should // always find a matching rule. throw ExceptionUtilities.Unreachable(); } } private static (IOperation? initializer, ISymbol? fieldOrProperty) TryFindFieldOrPropertyInitializerValue( Compilation compilation, IParameterSymbol parameter, CancellationToken cancellationToken) { foreach (var group in parameter.ContainingType.DeclaringSyntaxReferences.GroupBy(r => r.SyntaxTree)) { var semanticModel = compilation.GetSemanticModel(group.Key); foreach (var syntaxReference in group) { if (syntaxReference.GetSyntax(cancellationToken) is TypeDeclarationSyntax typeDeclaration) { foreach (var member in typeDeclaration.Members) { if (member is PropertyDeclarationSyntax { Initializer.Value: var propertyInitializer } propertyDeclaration) { var operation = semanticModel.GetOperation(propertyInitializer, cancellationToken); if (IsParameterReferenceOrCoalesceOfParameterReference(operation, parameter)) return (operation, semanticModel.GetRequiredDeclaredSymbol(propertyDeclaration, cancellationToken)); } else if (member is FieldDeclarationSyntax field) { foreach (var varDecl in field.Declaration.Variables) { if (varDecl is { Initializer.Value: var fieldInitializer }) { var operation = semanticModel.GetOperation(fieldInitializer, cancellationToken); if (IsParameterReferenceOrCoalesceOfParameterReference(operation, parameter)) return (operation, semanticModel.GetRequiredDeclaredSymbol(varDecl, cancellationToken)); } } } } } } } return default; } }