// 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.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.InitializeParameter; using static InitializeParameterHelpersCore; internal abstract class AbstractInitializerParameterService<TStatementSyntax> : IInitializeParameterService where TStatementSyntax : SyntaxNode { protected abstract bool IsFunctionDeclaration(SyntaxNode node); protected abstract SyntaxNode? GetAccessorBody(IMethodSymbol accessor, CancellationToken cancellationToken); protected abstract SyntaxNode GetBody(SyntaxNode methodNode); protected abstract SyntaxNode? TryGetLastStatement(IBlockOperation? blockStatement); protected abstract bool TryUpdateTupleAssignment(IBlockOperation? blockStatement, IParameterSymbol parameter, ISymbol fieldOrProperty, SyntaxEditor editor); protected abstract Task<Solution> TryAddAssignmentForPrimaryConstructorAsync( Document document, IParameterSymbol parameter, ISymbol fieldOrProperty, CancellationToken cancellationToken); protected abstract void InsertStatement( SyntaxEditor editor, SyntaxNode functionDeclaration, bool returnsVoid, SyntaxNode? statementToAddAfter, TStatementSyntax statement); public bool TryGetBlockForSingleParameterInitialization( SyntaxNode functionDeclaration, SemanticModel semanticModel, ISyntaxFactsService syntaxFacts, CancellationToken cancellationToken, out IBlockOperation? blockStatement) { blockStatement = null; var functionBody = GetBody(functionDeclaration); if (functionBody == null) { // We support initializing parameters, even when the containing member doesn't have a // body. This is useful for when the user is typing a new constructor and hasn't written // the body yet. return true; } // In order to get the block operation for the body of an anonymous function, we need to // get it via `IAnonymousFunctionOperation.Body` instead of getting it directly from the body syntax. var operation = semanticModel.GetOperation( syntaxFacts.IsAnonymousFunctionExpression(functionDeclaration) ? functionDeclaration : functionBody, cancellationToken); if (operation == null) return false; switch (operation.Kind) { case OperationKind.AnonymousFunction: blockStatement = ((IAnonymousFunctionOperation)operation).Body; break; case OperationKind.Block: blockStatement = (IBlockOperation)operation; break; default: return false; } return true; } public void InsertStatement(SyntaxEditor editor, SyntaxNode functionDeclaration, bool returnsVoid, SyntaxNode? statementToAddAfter, SyntaxNode statement) => InsertStatement(editor, functionDeclaration, returnsVoid, statementToAddAfter, (TStatementSyntax)statement); public async Task<Solution> AddAssignmentAsync( Document document, IParameterSymbol parameter, ISymbol fieldOrProperty, CancellationToken cancellationToken) { if (parameter is { DeclaringSyntaxReferences: [var parameterReference] }) { var parameterDeclaration = parameterReference.GetSyntax(cancellationToken); var functionDeclaration = parameterDeclaration.FirstAncestorOrSelf<SyntaxNode>(IsFunctionDeclaration); if (functionDeclaration is not null) { // try to handle the case where the parameter is within something function-like (like a constructor) return await TryAddAssignmentForFunctionLikeDeclarationAsync( document, parameter, fieldOrProperty, functionDeclaration, cancellationToken).ConfigureAwait(false); } else { // try to handle primary constructor case. return await TryAddAssignmentForPrimaryConstructorAsync( document, parameter, fieldOrProperty, cancellationToken).ConfigureAwait(false); } } return document.Project.Solution; } private async Task<Solution> TryAddAssignmentForFunctionLikeDeclarationAsync( Document document, IParameterSymbol parameter, ISymbol fieldOrProperty, SyntaxNode functionDeclaration, CancellationToken cancellationToken) { var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>(); var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); if (TryGetBlockForSingleParameterInitialization(functionDeclaration, semanticModel, syntaxFacts, cancellationToken, out var blockStatementOpt)) { var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var editor = new SyntaxEditor(root, document.Project.Solution.Services); AddAssignment(functionDeclaration, blockStatementOpt, parameter, fieldOrProperty, editor); var newDocument = document.WithSyntaxRoot(editor.GetChangedRoot()); return newDocument.Project.Solution; } return document.Project.Solution; } private void AddAssignment( SyntaxNode constructorDeclaration, IBlockOperation? blockStatement, IParameterSymbol parameter, ISymbol fieldOrProperty, SyntaxEditor editor) { // First see if the user has `(_x, y) = (x, y);` and attempt to update that. if (TryUpdateTupleAssignment(blockStatement, parameter, fieldOrProperty, editor)) return; var generator = editor.Generator; // Now that we've added any potential members, create an assignment between it // and the parameter. var initializationStatement = (TStatementSyntax)generator.ExpressionStatement( generator.AssignmentStatement( generator.MemberAccessExpression( generator.ThisExpression(), generator.IdentifierName(fieldOrProperty.Name)), generator.IdentifierName(parameter.Name))); // Attempt to place the initialization in a good location in the constructor // We'll want to keep initialization statements in the same order as we see // parameters for the constructor. var statementToAddAfter = TryGetStatementToAddInitializationAfter(parameter, blockStatement); InsertStatement(editor, constructorDeclaration, returnsVoid: true, statementToAddAfter, initializationStatement); } private SyntaxNode? TryGetStatementToAddInitializationAfter( IParameterSymbol parameter, IBlockOperation? blockStatement) { // look for an existing assignment for a parameter that comes before/after us. // If we find one, we'll add ourselves before/after that parameter check. foreach (var (sibling, before) in GetSiblingParameters(parameter)) { var statement = TryFindFieldOrPropertyAssignmentStatement(sibling, blockStatement); if (statement != null) { if (before) { return statement.Syntax; } else { var statementIndex = blockStatement!.Operations.IndexOf(statement); return statementIndex > 0 && blockStatement.Operations[statementIndex - 1] is { IsImplicit: false, Syntax: var priorSyntax } ? priorSyntax : null; } } } // We couldn't find a reasonable location for the new initialization statement. // Just place ourselves after the last statement in the constructor. return TryGetLastStatement(blockStatement); } public bool IsThrowNotImplementedProperty(Compilation compilation, IPropertySymbol property, CancellationToken cancellationToken) { using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var accessors); if (property.GetMethod != null) accessors.AddIfNotNull(GetAccessorBody(property.GetMethod, cancellationToken)); if (property.SetMethod != null) accessors.AddIfNotNull(GetAccessorBody(property.SetMethod, cancellationToken)); if (accessors.Count == 0) return false; foreach (var group in accessors.GroupBy(node => node.SyntaxTree)) { var semanticModel = compilation.GetSemanticModel(group.Key); foreach (var accessorBody in accessors) { var operation = semanticModel.GetOperation(accessorBody, cancellationToken); if (operation is null) return false; if (!operation.IsSingleThrowNotImplementedOperation()) return false; } } return true; } } |