// 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.Composition; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.ReplaceDiscardDeclarationsWithAssignments; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.ReplaceDiscardDeclarationsWithAssignments; using static SyntaxFactory; [ExportLanguageService(typeof(IReplaceDiscardDeclarationsWithAssignmentsService), LanguageNames.CSharp), Shared] internal sealed class CSharpReplaceDiscardDeclarationsWithAssignmentsService : IReplaceDiscardDeclarationsWithAssignmentsService { private const string DiscardVariableName = "_"; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public CSharpReplaceDiscardDeclarationsWithAssignmentsService() { } public async Task<SyntaxNode> ReplaceAsync( Document document, SyntaxNode memberDeclaration, CancellationToken cancellationToken) { var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var editor = new SyntaxEditor(memberDeclaration, document.Project.Solution.Services); foreach (var child in memberDeclaration.DescendantNodes()) { switch (child) { case LocalDeclarationStatementSyntax localDeclarationStatement: if (localDeclarationStatement.Declaration.Variables.Any(IsDiscardDeclaration)) { // Skip replacing discard declarations in "using var" if (localDeclarationStatement.UsingKeyword != default) { continue; } RemoveDiscardHelper.ProcessDeclarationStatement(localDeclarationStatement, editor); } break; case CatchDeclarationSyntax catchDeclaration: if (IsDiscardDeclaration(catchDeclaration)) { // "catch (Exception _)" => "catch (Exception)" editor.ReplaceNode(catchDeclaration, catchDeclaration.WithIdentifier(default)); } break; case DeclarationExpressionSyntax declarationExpression: if (declarationExpression.Designation is DiscardDesignationSyntax discardSyntax) { // "M(out var _)" => "M(out _)" // "M(out int _)" => "M(out _)" var discardToken = Identifier( leading: declarationExpression.GetLeadingTrivia(), contextualKind: SyntaxKind.UnderscoreToken, text: discardSyntax.UnderscoreToken.Text, valueText: discardSyntax.UnderscoreToken.ValueText, trailing: declarationExpression.GetTrailingTrivia()); var replacementNode = IdentifierName(discardToken); // Removing explicit type is possible only if there are no overloads of the method with same parameter. // For example, if method "M" had overloads with signature "void M(int x)" and "void M(char x)", // then the replacement "M(out int _)" => "M(out _)" will cause overload resolution error. // Bail out if replacement changes semantics. var speculationAnalyzer = new SpeculationAnalyzer(declarationExpression, replacementNode, semanticModel, cancellationToken); if (!speculationAnalyzer.ReplacementChangesSemantics()) { editor.ReplaceNode(declarationExpression, replacementNode); } } break; case DeclarationPatternSyntax declarationPattern: if (declarationPattern.Designation is DiscardDesignationSyntax discardDesignationSyntax && declarationPattern.Parent is IsPatternExpressionSyntax isPatternExpression) { // "x is int _" => "x is int" var replacementNode = BinaryExpression( kind: SyntaxKind.IsExpression, left: isPatternExpression.Expression, operatorToken: isPatternExpression.IsKeyword, right: declarationPattern.Type.WithTrailingTrivia(declarationPattern.GetTrailingTrivia())); editor.ReplaceNode(isPatternExpression, replacementNode); } break; } } return editor.GetChangedRoot(); } private static bool IsDiscardDeclaration(VariableDeclaratorSyntax variable) => variable.Identifier.Text == DiscardVariableName; private static bool IsDiscardDeclaration(CatchDeclarationSyntax catchDeclaration) => catchDeclaration.Identifier.Text == DiscardVariableName; private sealed class RemoveDiscardHelper : IDisposable { private readonly LocalDeclarationStatementSyntax _localDeclarationStatement; private readonly SyntaxEditor _editor; private readonly ArrayBuilder<StatementSyntax> _statementsBuilder; private SeparatedSyntaxList<VariableDeclaratorSyntax> _currentNonDiscardVariables = []; private RemoveDiscardHelper(LocalDeclarationStatementSyntax localDeclarationStatement, SyntaxEditor editor) { _localDeclarationStatement = localDeclarationStatement; _editor = editor; _statementsBuilder = ArrayBuilder<StatementSyntax>.GetInstance(); } public static void ProcessDeclarationStatement( LocalDeclarationStatementSyntax localDeclarationStatement, SyntaxEditor editor) { using var helper = new RemoveDiscardHelper(localDeclarationStatement, editor); helper.ProcessDeclarationStatement(); } public void Dispose() => _statementsBuilder.Free(); private void ProcessDeclarationStatement() { // We will replace all discard variable declarations in this method with discard assignments, // For example, // 1. "int _ = M();" is replaced with "_ = M();" // 2. "int x = 1, _ = M(), y = 2;" is replaced with following statements: // int x = 1; // _ = M(); // int y = 2; // This is done to prevent compiler errors where the existing method has a discard // variable declaration at a line following the one we added a discard assignment in our fix. // Process all the declared variables in the given local declaration statement, // tracking the currently encountered non-discard variables. foreach (var variable in _localDeclarationStatement.Declaration.Variables) { if (!IsDiscardDeclaration(variable)) { // Add to the list of currently encountered non-discard variables _currentNonDiscardVariables = _currentNonDiscardVariables.Add(variable); } else { // Process currently encountered non-discard variables to generate // a local declaration statement with these variables. GenerateDeclarationStatementForCurrentNonDiscardVariables(); // Process the discard variable declaration to replace it // with an assignment to discard. GenerateAssignmentForDiscardVariable(variable); } } // Process all the remaining variable declarators to generate // a local declaration statement with these variables. GenerateDeclarationStatementForCurrentNonDiscardVariables(); // Now replace the original local declaration statement with // the replacement statement list tracked in _statementsBuilder. if (_statementsBuilder.Count == 0) { // Nothing to replace. return; } // Move the leading trivia from original local declaration statement // to the first statement of the replacement statement list. var leadingTrivia = _localDeclarationStatement.Declaration.Type.GetLeadingTrivia() .Concat(_localDeclarationStatement.Declaration.Type.GetTrailingTrivia()); _statementsBuilder[0] = _statementsBuilder[0].WithLeadingTrivia(leadingTrivia); // Move the trailing trivia from original local declaration statement // to the last statement of the replacement statement list. var last = _statementsBuilder.Count - 1; var trailingTrivia = _localDeclarationStatement.SemicolonToken.GetAllTrivia(); _statementsBuilder[last] = _statementsBuilder[last].WithTrailingTrivia(trailingTrivia); // Replace the original local declaration statement with new statement list // from _statementsBuilder. if (_localDeclarationStatement.Parent is BlockSyntax or SwitchSectionSyntax) { if (_statementsBuilder.Count > 1) { _editor.InsertAfter(_localDeclarationStatement, _statementsBuilder.Skip(1)); } _editor.ReplaceNode(_localDeclarationStatement, _statementsBuilder[0]); } else { _editor.ReplaceNode(_localDeclarationStatement, Block(_statementsBuilder)); } } private void GenerateDeclarationStatementForCurrentNonDiscardVariables() { // Generate a variable declaration with all the currently tracked non-discard declarators. // For example, for a declaration "int x = 1, y = 2, _ = M(), z = 3;", we generate two variable declarations: // 1. "int x = 1, y = 2;" and // 2. "int z = 3;", // which are split by a single assignment statement "_ = M();" if (_currentNonDiscardVariables.Count > 0) { var statement = LocalDeclarationStatement( VariableDeclaration(_localDeclarationStatement.Declaration.Type, _currentNonDiscardVariables)) .WithAdditionalAnnotations(Formatter.Annotation); _statementsBuilder.Add(statement); _currentNonDiscardVariables = []; } } private void GenerateAssignmentForDiscardVariable(VariableDeclaratorSyntax variable) { Debug.Assert(IsDiscardDeclaration(variable)); // Convert a discard declaration with initializer of the form "int _ = M();" into // a discard assignment "_ = M();" if (variable.Initializer != null) { _statementsBuilder.Add( ExpressionStatement( AssignmentExpression( kind: SyntaxKind.SimpleAssignmentExpression, left: IdentifierName(variable.Identifier), operatorToken: variable.Initializer.EqualsToken, right: variable.Initializer.Value)) .WithAdditionalAnnotations(Formatter.Annotation)); } } } } |