diff --git a/src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems b/src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems index 9c3b89c6fed3c..52f82c2892dfb 100644 --- a/src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems +++ b/src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems @@ -81,6 +81,7 @@ + diff --git a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer.cs index eedf9668b1e97..41c1aeec94769 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer.cs @@ -4,29 +4,22 @@ using System.Collections.Immutable; using System.Diagnostics; -using System.Linq; -using System.Threading; using Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.CSharp.Analyzers.UseCollectionExpression; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Shared.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.CSharp.Utilities; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.CSharp.UseCollectionExpression; -using static SyntaxFactory; - [DiagnosticAnalyzer(LanguageNames.CSharp)] internal sealed partial class CSharpUseCollectionExpressionForArrayDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { - private static readonly CollectionExpressionSyntax s_emptyCollectionExpression = CollectionExpression(); - public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; @@ -56,7 +49,7 @@ protected override void InitializeWorker(AnalysisContext context) private void OnCompilationStart(CompilationStartAnalysisContext context) { - if (!context.Compilation.LanguageVersion().IsCSharp12OrAbove()) + if (!context.Compilation.LanguageVersion().SupportsCollectionExpressions()) return; // We wrap the SyntaxNodeAction within a CodeBlockStartAction, which allows us to @@ -72,101 +65,6 @@ private void OnCompilationStart(CompilationStartAnalysisContext context) }); } - private static bool IsInTargetTypedLocation(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) - { - var topExpression = expression.WalkUpParentheses(); - var parent = topExpression.Parent; - return parent switch - { - EqualsValueClauseSyntax equalsValue => IsInTargetTypedEqualsValueClause(equalsValue), - CastExpressionSyntax castExpression => IsInTargetTypedCastExpression(castExpression), - // a ? [1, 2, 3] : ... is target typed if either the other side is *not* a collection, - // or the entire ternary is target typed itself. - ConditionalExpressionSyntax conditionalExpression => IsInTargetTypedConditionalExpression(conditionalExpression, topExpression), - // Similar rules for switches. - SwitchExpressionArmSyntax switchExpressionArm => IsInTargetTypedSwitchExpressionArm(switchExpressionArm), - InitializerExpressionSyntax initializerExpression => IsInTargetTypedInitializerExpression(initializerExpression, topExpression), - AssignmentExpressionSyntax assignmentExpression => IsInTargetTypedAssignmentExpression(assignmentExpression, topExpression), - BinaryExpressionSyntax binaryExpression => IsInTargetTypedBinaryExpression(binaryExpression, topExpression), - ArgumentSyntax or AttributeArgumentSyntax => true, - ReturnStatementSyntax => true, - _ => false, - }; - - bool HasType(ExpressionSyntax expression) - => semanticModel.GetTypeInfo(expression, cancellationToken).Type != null; - - static bool IsInTargetTypedEqualsValueClause(EqualsValueClauseSyntax equalsValue) - // If we're after an `x = ...` and it's not `var x`, this is target typed. - => equalsValue.Parent is not VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Type.IsVar: true } }; - - static bool IsInTargetTypedCastExpression(CastExpressionSyntax castExpression) - // (X[])[1, 2, 3] is target typed. `(X)[1, 2, 3]` is currently not (because it looks like indexing into an expr). - => castExpression.Type is not IdentifierNameSyntax; - - bool IsInTargetTypedConditionalExpression(ConditionalExpressionSyntax conditionalExpression, ExpressionSyntax expression) - { - if (conditionalExpression.WhenTrue == expression) - return HasType(conditionalExpression.WhenFalse) || IsInTargetTypedLocation(semanticModel, conditionalExpression, cancellationToken); - else if (conditionalExpression.WhenFalse == expression) - return HasType(conditionalExpression.WhenTrue) || IsInTargetTypedLocation(semanticModel, conditionalExpression, cancellationToken); - else - return false; - } - - bool IsInTargetTypedSwitchExpressionArm(SwitchExpressionArmSyntax switchExpressionArm) - { - var switchExpression = (SwitchExpressionSyntax)switchExpressionArm.GetRequiredParent(); - - // check if any other arm has a type that this would be target typed against. - foreach (var arm in switchExpression.Arms) - { - if (arm != switchExpressionArm && HasType(arm.Expression)) - return true; - } - - // All arms do not have a type, this is target typed if the switch itself is target typed. - return IsInTargetTypedLocation(semanticModel, switchExpression, cancellationToken); - } - - bool IsInTargetTypedInitializerExpression(InitializerExpressionSyntax initializerExpression, ExpressionSyntax expression) - { - // new X[] { [1, 2, 3] }. Elements are target typed by array type. - if (initializerExpression.Parent is ArrayCreationExpressionSyntax) - return true; - - // new [] { [1, 2, 3], ... }. Elements are target typed if there's another element with real type. - if (initializerExpression.Parent is ImplicitArrayCreationExpressionSyntax) - { - foreach (var sibling in initializerExpression.Expressions) - { - if (sibling != expression && HasType(sibling)) - return true; - } - } - - // TODO: Handle these. - if (initializerExpression.Parent is StackAllocArrayCreationExpressionSyntax or ImplicitStackAllocArrayCreationExpressionSyntax) - return false; - - // T[] x = [1, 2, 3]; - if (initializerExpression.Parent is EqualsValueClauseSyntax) - return true; - - return false; - } - - bool IsInTargetTypedAssignmentExpression(AssignmentExpressionSyntax assignmentExpression, ExpressionSyntax expression) - { - return expression == assignmentExpression.Right && HasType(assignmentExpression.Left); - } - - bool IsInTargetTypedBinaryExpression(BinaryExpressionSyntax binaryExpression, ExpressionSyntax expression) - { - return binaryExpression.Kind() == SyntaxKind.CoalesceExpression && binaryExpression.Right == expression && HasType(binaryExpression.Left); - } - } - private static void AnalyzeArrayInitializer(SyntaxNodeAnalysisContext context) { var semanticModel = context.SemanticModel; @@ -179,74 +77,38 @@ private static void AnalyzeArrayInitializer(SyntaxNodeAnalysisContext context) if (!option.Value) return; - if (initializer.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) - return; - - var parent = initializer.GetRequiredParent(); - var topmostExpression = parent is ExpressionSyntax parentExpression - ? parentExpression.WalkUpParentheses() - : initializer.WalkUpParentheses(); + var isConcreteOrImplicitArrayCreation = initializer.Parent is ArrayCreationExpressionSyntax or ImplicitArrayCreationExpressionSyntax; - if (!IsInTargetTypedLocation(semanticModel, topmostExpression, cancellationToken)) + // a naked `{ ... }` can only be converted to a collection expression when in the exact form `x = { ... }` + if (!isConcreteOrImplicitArrayCreation && initializer.Parent is not EqualsValueClauseSyntax) return; - var isConcreteOrImplicitArrayCreation = parent is ArrayCreationExpressionSyntax or ImplicitArrayCreationExpressionSyntax; - if (isConcreteOrImplicitArrayCreation) - { - // X[] = new Y[] { 1, 2, 3 } - // - // First, we don't change things if X and Y are different. That could lead to something observable at - // runtime in the case of something like: object[] x = new string[] ... - - var typeInfo = semanticModel.GetTypeInfo(parent, cancellationToken); - if (typeInfo.Type is null or IErrorTypeSymbol || - typeInfo.ConvertedType is null or IErrorTypeSymbol) - { - return; - } + var arrayCreationExpression = isConcreteOrImplicitArrayCreation + ? (ExpressionSyntax)initializer.GetRequiredParent() + : initializer; - if (!typeInfo.Type.Equals(typeInfo.ConvertedType)) - return; - } - else if (parent is not EqualsValueClauseSyntax) + if (!UseCollectionExpressionHelpers.CanReplaceWithCollectionExpression( + semanticModel, arrayCreationExpression, cancellationToken)) { return; } - // Looks good as something to replace. Now check the semantics of making the replacement to see if there would - // any issues. To keep things simple, all we do is replace the existing expression with the `[]` literal. This - // will tell us if we have problems assigning a collection expression to teh target type. - // - // Note: this does mean certain unambiguous cases with overloads (like `Goo(int[] values)` vs `Goo(string[] - // values)`) will not get simplification. We can revisit this in the future to see if that warrants a more - // expensive check that involves checking the consitutuent elements of the literal. - var speculationAnalyzer = new SpeculationAnalyzer( - topmostExpression, - s_emptyCollectionExpression, - semanticModel, - cancellationToken, - skipVerificationForReplacedNode: false, - failOnOverloadResolutionFailuresInOriginalCode: true); - - if (speculationAnalyzer.ReplacementChangesSemantics()) - return; - if (isConcreteOrImplicitArrayCreation) { var locations = ImmutableArray.Create(initializer.GetLocation()); context.ReportDiagnostic(DiagnosticHelper.Create( s_descriptor, - parent.GetFirstToken().GetLocation(), + arrayCreationExpression.GetFirstToken().GetLocation(), option.Notification.Severity, additionalLocations: locations, properties: null)); var additionalUnnecessaryLocations = ImmutableArray.Create( syntaxTree.GetLocation(TextSpan.FromBounds( - parent.SpanStart, - parent is ArrayCreationExpressionSyntax arrayCreation + arrayCreationExpression.SpanStart, + arrayCreationExpression is ArrayCreationExpressionSyntax arrayCreation ? arrayCreation.Type.Span.End - : ((ImplicitArrayCreationExpressionSyntax)parent).CloseBracketToken.Span.End))); + : ((ImplicitArrayCreationExpressionSyntax)arrayCreationExpression).CloseBracketToken.Span.End))); context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( s_unnecessaryCodeDescriptor, @@ -257,7 +119,7 @@ parent is ArrayCreationExpressionSyntax arrayCreation } else { - Debug.Assert(parent is EqualsValueClauseSyntax); + Debug.Assert(initializer.Parent is EqualsValueClauseSyntax); // int[] = { 1, 2, 3 }; // // In this case, we always have a target type, so it should always be valid to convert this to a collection expression. diff --git a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/UseCollectionExpressionHelpers.cs b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/UseCollectionExpressionHelpers.cs new file mode 100644 index 0000000000000..3614cdf460a0e --- /dev/null +++ b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/UseCollectionExpressionHelpers.cs @@ -0,0 +1,162 @@ +// 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 Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp.Utilities; +using Microsoft.CodeAnalysis.Shared.Extensions; + +namespace Microsoft.CodeAnalysis.CSharp.Analyzers.UseCollectionExpression; + +using static SyntaxFactory; + +internal static class UseCollectionExpressionHelpers +{ + private static readonly LiteralExpressionSyntax s_nullLiteralExpression = LiteralExpression(SyntaxKind.NullLiteralExpression); + + public static bool CanReplaceWithCollectionExpression( + SemanticModel semanticModel, + ExpressionSyntax expression, + CancellationToken cancellationToken) + { + var topMostExpression = expression.WalkUpParentheses(); + if (topMostExpression.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) + return false; + + var parent = topMostExpression.GetRequiredParent(); + + if (!IsInTargetTypedLocation(semanticModel, topMostExpression, cancellationToken)) + return false; + + // X[] = new Y[] { 1, 2, 3 } + // + // First, we don't change things if X and Y are different. That could lead to something observable at + // runtime in the case of something like: object[] x = new string[] ... + + var typeInfo = semanticModel.GetTypeInfo(topMostExpression, cancellationToken); + if (typeInfo.Type is IErrorTypeSymbol) + return false; + + if (typeInfo.ConvertedType is null or IErrorTypeSymbol) + return false; + + if (typeInfo.Type != null && !typeInfo.Type.Equals(typeInfo.ConvertedType)) + return false; + + // Looks good as something to replace. Now check the semantics of making the replacement to see if there would + // any issues. To keep things simple, all we do is replace the existing expression with the `null` literal. + // This is a similarly 'untyped' literal (like a collection-expression is), so it tells us if the new code will + // have any issues moving to something untyped. This will also tell us if we have any ambiguities (because + // there are multiple destination types that could accept the collection expression). + var speculationAnalyzer = new SpeculationAnalyzer( + topMostExpression, + s_nullLiteralExpression, + semanticModel, + cancellationToken, + skipVerificationForReplacedNode: true, + failOnOverloadResolutionFailuresInOriginalCode: true); + + if (speculationAnalyzer.ReplacementChangesSemantics()) + return false; + + return true; + } + + private static bool IsInTargetTypedLocation(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) + { + var topExpression = expression.WalkUpParentheses(); + var parent = topExpression.Parent; + return parent switch + { + EqualsValueClauseSyntax equalsValue => IsInTargetTypedEqualsValueClause(equalsValue), + CastExpressionSyntax castExpression => IsInTargetTypedCastExpression(castExpression), + // a ? [1, 2, 3] : ... is target typed if either the other side is *not* a collection, + // or the entire ternary is target typed itself. + ConditionalExpressionSyntax conditionalExpression => IsInTargetTypedConditionalExpression(conditionalExpression, topExpression), + // Similar rules for switches. + SwitchExpressionArmSyntax switchExpressionArm => IsInTargetTypedSwitchExpressionArm(switchExpressionArm), + InitializerExpressionSyntax initializerExpression => IsInTargetTypedInitializerExpression(initializerExpression, topExpression), + AssignmentExpressionSyntax assignmentExpression => IsInTargetTypedAssignmentExpression(assignmentExpression, topExpression), + BinaryExpressionSyntax binaryExpression => IsInTargetTypedBinaryExpression(binaryExpression, topExpression), + ArgumentSyntax or AttributeArgumentSyntax => true, + ReturnStatementSyntax => true, + _ => false, + }; + + bool HasType(ExpressionSyntax expression) + => semanticModel.GetTypeInfo(expression, cancellationToken).Type != null; + + static bool IsInTargetTypedEqualsValueClause(EqualsValueClauseSyntax equalsValue) + // If we're after an `x = ...` and it's not `var x`, this is target typed. + => equalsValue.Parent is not VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Type.IsVar: true } }; + + static bool IsInTargetTypedCastExpression(CastExpressionSyntax castExpression) + // (X[])[1, 2, 3] is target typed. `(X)[1, 2, 3]` is currently not (because it looks like indexing into an expr). + => castExpression.Type is not IdentifierNameSyntax; + + bool IsInTargetTypedConditionalExpression(ConditionalExpressionSyntax conditionalExpression, ExpressionSyntax expression) + { + if (conditionalExpression.WhenTrue == expression) + return HasType(conditionalExpression.WhenFalse) || IsInTargetTypedLocation(semanticModel, conditionalExpression, cancellationToken); + else if (conditionalExpression.WhenFalse == expression) + return HasType(conditionalExpression.WhenTrue) || IsInTargetTypedLocation(semanticModel, conditionalExpression, cancellationToken); + else + return false; + } + + bool IsInTargetTypedSwitchExpressionArm(SwitchExpressionArmSyntax switchExpressionArm) + { + var switchExpression = (SwitchExpressionSyntax)switchExpressionArm.GetRequiredParent(); + + // check if any other arm has a type that this would be target typed against. + foreach (var arm in switchExpression.Arms) + { + if (arm != switchExpressionArm && HasType(arm.Expression)) + return true; + } + + // All arms do not have a type, this is target typed if the switch itself is target typed. + return IsInTargetTypedLocation(semanticModel, switchExpression, cancellationToken); + } + + bool IsInTargetTypedInitializerExpression(InitializerExpressionSyntax initializerExpression, ExpressionSyntax expression) + { + // new X[] { [1, 2, 3] }. Elements are target typed by array type. + if (initializerExpression.Parent is ArrayCreationExpressionSyntax) + return true; + + // new [] { [1, 2, 3], ... }. Elements are target typed if there's another element with real type. + if (initializerExpression.Parent is ImplicitArrayCreationExpressionSyntax) + { + foreach (var sibling in initializerExpression.Expressions) + { + if (sibling != expression && HasType(sibling)) + return true; + } + } + + // TODO: Handle these. + if (initializerExpression.Parent is StackAllocArrayCreationExpressionSyntax or ImplicitStackAllocArrayCreationExpressionSyntax) + return false; + + // T[] x = [1, 2, 3]; + if (initializerExpression.Parent is EqualsValueClauseSyntax) + return true; + + return false; + } + + bool IsInTargetTypedAssignmentExpression(AssignmentExpressionSyntax assignmentExpression, ExpressionSyntax expression) + { + return expression == assignmentExpression.Right && HasType(assignmentExpression.Left); + } + + bool IsInTargetTypedBinaryExpression(BinaryExpressionSyntax binaryExpression, ExpressionSyntax expression) + { + return binaryExpression.Kind() == SyntaxKind.CoalesceExpression && binaryExpression.Right == expression && HasType(binaryExpression.Left); + } + } +} diff --git a/src/Analyzers/CSharp/Analyzers/UseCollectionInitializer/CSharpUseCollectionInitializerDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCollectionInitializer/CSharpUseCollectionInitializerDiagnosticAnalyzer.cs index 8a3cdd0f30d61..c77375394ff8f 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCollectionInitializer/CSharpUseCollectionInitializerDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCollectionInitializer/CSharpUseCollectionInitializerDiagnosticAnalyzer.cs @@ -2,8 +2,11 @@ // 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.Threading; +using Microsoft.CodeAnalysis.CSharp.Analyzers.UseCollectionExpression; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.LanguageService; +using Microsoft.CodeAnalysis.CSharp.Shared.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageService; @@ -21,11 +24,21 @@ internal class CSharpUseCollectionInitializerDiagnosticAnalyzer : MemberAccessExpressionSyntax, InvocationExpressionSyntax, ExpressionStatementSyntax, + ForEachStatementSyntax, VariableDeclaratorSyntax> { protected override bool AreCollectionInitializersSupported(Compilation compilation) => compilation.LanguageVersion() >= LanguageVersion.CSharp3; + protected override bool AreCollectionExpressionsSupported(Compilation compilation) + => compilation.LanguageVersion().SupportsCollectionExpressions(); + protected override ISyntaxFacts GetSyntaxFacts() => CSharpSyntaxFacts.Instance; + + protected override bool CanUseCollectionExpression + (SemanticModel semanticModel, BaseObjectCreationExpressionSyntax objectCreationExpression, CancellationToken cancellationToken) + { + return UseCollectionExpressionHelpers.CanReplaceWithCollectionExpression(semanticModel, objectCreationExpression, cancellationToken); + } } } diff --git a/src/Analyzers/CSharp/Analyzers/UsePrimaryConstructor/CSharpUsePrimaryConstructorDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UsePrimaryConstructor/CSharpUsePrimaryConstructorDiagnosticAnalyzer.cs index 919a4862761c9..64ad93cb23a7e 100644 --- a/src/Analyzers/CSharp/Analyzers/UsePrimaryConstructor/CSharpUsePrimaryConstructorDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UsePrimaryConstructor/CSharpUsePrimaryConstructorDiagnosticAnalyzer.cs @@ -73,9 +73,7 @@ protected override void InitializeWorker(AnalysisContext context) { context.RegisterCompilationStartAction(context => { - // "x is not Type y" is only available in C# 9.0 and above. Don't offer this refactoring - // in projects targeting a lesser version. - if (!context.Compilation.LanguageVersion().IsCSharp12OrAbove()) + if (!context.Compilation.LanguageVersion().SupportsPrimaryConstructors()) return; // Mapping from a named type to a particular analyzer we have created for it. Needed because nested diff --git a/src/Analyzers/CSharp/CodeFixes/UseCollectionInitializer/CSharpUseCollectionInitializerCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseCollectionInitializer/CSharpUseCollectionInitializerCodeFixProvider.cs index 931a70bb5063d..469fb3530944a 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseCollectionInitializer/CSharpUseCollectionInitializerCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseCollectionInitializer/CSharpUseCollectionInitializerCodeFixProvider.cs @@ -7,16 +7,21 @@ using System.Composition; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.UseObjectInitializer; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.UseCollectionInitializer; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.UseCollectionInitializer { + using static SyntaxFactory; + [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseCollectionInitializer), Shared] internal class CSharpUseCollectionInitializerCodeFixProvider : AbstractUseCollectionInitializerCodeFixProvider< @@ -27,6 +32,7 @@ internal class CSharpUseCollectionInitializerCodeFixProvider : MemberAccessExpressionSyntax, InvocationExpressionSyntax, ExpressionStatementSyntax, + ForEachStatementSyntax, VariableDeclaratorSyntax> { [ImportingConstructor] @@ -36,80 +42,67 @@ public CSharpUseCollectionInitializerCodeFixProvider() } protected override StatementSyntax GetNewStatement( + SourceText sourceText, StatementSyntax statement, BaseObjectCreationExpressionSyntax objectCreation, - ImmutableArray matches) + int wrappingLength, + bool useCollectionExpression, + ImmutableArray> matches) { return statement.ReplaceNode( objectCreation, - GetNewObjectCreation(objectCreation, matches)); + GetNewObjectCreation(sourceText, objectCreation, wrappingLength, useCollectionExpression, matches)); } - private static BaseObjectCreationExpressionSyntax GetNewObjectCreation( + private static ExpressionSyntax GetNewObjectCreation( + SourceText sourceText, BaseObjectCreationExpressionSyntax objectCreation, - ImmutableArray matches) + int wrappingLength, + bool useCollectionExpression, + ImmutableArray> matches) { - return UseInitializerHelpers.GetNewObjectCreation( - objectCreation, CreateExpressions(objectCreation, matches)); + return useCollectionExpression + ? CreateCollectionExpression(objectCreation, matches, MakeMultiLine(sourceText, objectCreation, matches, wrappingLength)) + : CreateObjectInitializerExpression(objectCreation, matches); } - private static SeparatedSyntaxList CreateExpressions( + private static BaseObjectCreationExpressionSyntax CreateObjectInitializerExpression( BaseObjectCreationExpressionSyntax objectCreation, - ImmutableArray matches) + ImmutableArray> matches) { - using var _ = ArrayBuilder.GetInstance(out var nodesAndTokens); - - UseInitializerHelpers.AddExistingItems(objectCreation, nodesAndTokens); - - for (var i = 0; i < matches.Length; i++) - { - var expressionStatement = matches[i]; - var trivia = expressionStatement.GetLeadingTrivia(); - - var newTrivia = i == 0 ? trivia.WithoutLeadingBlankLines() : trivia; - - var newExpression = ConvertExpression(expressionStatement.Expression) - .WithoutTrivia() - .WithPrependedLeadingTrivia(newTrivia); + var expressions = CreateElements(objectCreation, matches, static (_, e) => e); + var withLineBreaks = AddLineBreaks(expressions, includeFinalLineBreak: true); + return UseInitializerHelpers.GetNewObjectCreation(objectCreation, withLineBreaks); + } - if (i < matches.Length - 1) - { - nodesAndTokens.Add(newExpression); - var commaToken = SyntaxFactory.Token(SyntaxKind.CommaToken) - .WithTriviaFrom(expressionStatement.SemicolonToken); + private static CollectionExpressionSyntax CreateCollectionExpression( + BaseObjectCreationExpressionSyntax objectCreation, + ImmutableArray> matches, + bool makeMultiLine) + { + var elements = CreateElements( + objectCreation, matches, + static (match, expression) => match?.UseSpread is true ? SpreadElement(expression) : ExpressionElement(expression)); - nodesAndTokens.Add(commaToken); - } - else - { - newExpression = newExpression.WithTrailingTrivia( - expressionStatement.GetTrailingTrivia()); - nodesAndTokens.Add(newExpression); - } - } + if (makeMultiLine) + elements = AddLineBreaks(elements, includeFinalLineBreak: false); - return SyntaxFactory.SeparatedList(nodesAndTokens); + return CollectionExpression(elements).WithTriviaFrom(objectCreation); } private static ExpressionSyntax ConvertExpression(ExpressionSyntax expression) - { - if (expression is InvocationExpressionSyntax invocation) + => expression switch { - return ConvertInvocation(invocation); - } - else if (expression is AssignmentExpressionSyntax assignment) - { - return ConvertAssignment(assignment); - } - - throw new InvalidOperationException(); - } + InvocationExpressionSyntax invocation => ConvertInvocation(invocation), + AssignmentExpressionSyntax assignment => ConvertAssignment(assignment), + _ => throw new InvalidOperationException(), + }; - private static ExpressionSyntax ConvertAssignment(AssignmentExpressionSyntax assignment) + private static AssignmentExpressionSyntax ConvertAssignment(AssignmentExpressionSyntax assignment) { var elementAccess = (ElementAccessExpressionSyntax)assignment.Left; return assignment.WithLeft( - SyntaxFactory.ImplicitElementAccess(elementAccess.ArgumentList)); + ImplicitElementAccess(elementAccess.ArgumentList)); } private static ExpressionSyntax ConvertInvocation(InvocationExpressionSyntax invocation) @@ -124,17 +117,137 @@ private static ExpressionSyntax ConvertInvocation(InvocationExpressionSyntax inv // avoid the ambiguity. var expression = arguments[0].Expression; return SyntaxFacts.IsAssignmentExpression(expression.Kind()) - ? SyntaxFactory.ParenthesizedExpression(expression) + ? ParenthesizedExpression(expression) : expression; } - return SyntaxFactory.InitializerExpression( + return InitializerExpression( SyntaxKind.ComplexElementInitializerExpression, - SyntaxFactory.Token(SyntaxKind.OpenBraceToken).WithoutTrivia(), - SyntaxFactory.SeparatedList( + Token(SyntaxKind.OpenBraceToken).WithoutTrivia(), + SeparatedList( arguments.Select(a => a.Expression), arguments.GetSeparators()), - SyntaxFactory.Token(SyntaxKind.CloseBraceToken).WithoutTrivia()); + Token(SyntaxKind.CloseBraceToken).WithoutTrivia()); + } + + private static bool MakeMultiLine( + SourceText sourceText, + BaseObjectCreationExpressionSyntax objectCreation, + ImmutableArray> matches, + int wrappingLength) + { + // If it's already multiline, keep it that way. + if (!sourceText.AreOnSameLine(objectCreation.GetFirstToken(), objectCreation.GetLastToken())) + return true; + + foreach (var match in matches) + { + var expression = GetExpression(match); + + // If we have anything like: `new Dictionary { { A, B }, { C, D } }` then always make multiline. + // Similarly, if we have `new Dictionary { [A] = B }`. + if (expression is InitializerExpressionSyntax or AssignmentExpressionSyntax) + return true; + + // if any of the expressions we're adding are multiline, then make things multiline. + if (!sourceText.AreOnSameLine(expression.GetFirstToken(), expression.GetLastToken())) + return true; + } + + var totalLength = "{}".Length; + foreach (var match in matches) + { + var expression = GetExpression(match); + totalLength += expression.Span.Length; + totalLength += ", ".Length; + + if (totalLength > wrappingLength) + return true; + } + + return false; + + static ExpressionSyntax GetExpression(Match match) + => match.Statement switch + { + ExpressionStatementSyntax expressionStatement => expressionStatement.Expression, + ForEachStatementSyntax foreachStatement => foreachStatement.Expression, + _ => throw ExceptionUtilities.Unreachable(), + }; + } + + public static SeparatedSyntaxList AddLineBreaks( + SeparatedSyntaxList nodes, bool includeFinalLineBreak) + where TNode : SyntaxNode + { + using var _ = ArrayBuilder.GetInstance(out var nodesAndTokens); + + var nodeOrTokenList = nodes.GetWithSeparators(); + foreach (var item in nodeOrTokenList) + { + var addLineBreak = item.IsToken || (includeFinalLineBreak && item == nodeOrTokenList.Last()); + if (addLineBreak && item.GetTrailingTrivia() is not [.., (kind: SyntaxKind.EndOfLineTrivia)]) + { + nodesAndTokens.Add(item.WithAppendedTrailingTrivia(ElasticCarriageReturnLineFeed)); + } + else + { + nodesAndTokens.Add(item); + } + } + + return SeparatedList(nodesAndTokens); + } + + private static SeparatedSyntaxList CreateElements( + BaseObjectCreationExpressionSyntax objectCreation, + ImmutableArray> matches, + Func?, ExpressionSyntax, TElement> createElement) + where TElement : SyntaxNode + { + using var _ = ArrayBuilder.GetInstance(out var nodesAndTokens); + + UseInitializerHelpers.AddExistingItems(objectCreation, nodesAndTokens, createElement); + + for (var i = 0; i < matches.Length; i++) + { + var match = matches[i]; + var statement = match.Statement; + + if (statement is ExpressionStatementSyntax expressionStatement) + { + var trivia = statement.GetLeadingTrivia(); + var leadingTrivia = i == 0 ? trivia.WithoutLeadingBlankLines() : trivia; + + var semicolon = expressionStatement.SemicolonToken; + var trailingTrivia = semicolon.TrailingTrivia.Contains(static t => t.IsSingleOrMultiLineComment()) + ? semicolon.TrailingTrivia + : default; + + var expression = createElement(match, ConvertExpression(expressionStatement.Expression).WithoutTrivia()).WithLeadingTrivia(leadingTrivia); + if (i < matches.Length - 1) + { + nodesAndTokens.Add(expression); + nodesAndTokens.Add(Token(SyntaxKind.CommaToken).WithTrailingTrivia(trailingTrivia)); + } + else + { + nodesAndTokens.Add(expression.WithTrailingTrivia(trailingTrivia)); + } + } + else if (statement is ForEachStatementSyntax foreachStatement) + { + nodesAndTokens.Add(createElement(match, foreachStatement.Expression.WithoutTrivia())); + if (i < matches.Length - 1) + nodesAndTokens.Add(Token(SyntaxKind.CommaToken)); + } + else + { + throw ExceptionUtilities.Unreachable(); + } + } + + return SeparatedList(nodesAndTokens); } } } diff --git a/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/MultiLineConditionalExpressionFormattingRule.cs b/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/MultiLineConditionalExpressionFormattingRule.cs index 4c687000ffcb8..12f3a88dccbe9 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/MultiLineConditionalExpressionFormattingRule.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseConditionalExpression/MultiLineConditionalExpressionFormattingRule.cs @@ -32,15 +32,7 @@ private MultiLineConditionalExpressionFormattingRule() } private static bool IsQuestionOrColonOfNewConditional(SyntaxToken token) - { - if (token.Kind() is SyntaxKind.QuestionToken or - SyntaxKind.ColonToken) - { - return token.Parent.HasAnnotation(SpecializedFormattingAnnotation); - } - - return false; - } + => token.Kind() is SyntaxKind.QuestionToken or SyntaxKind.ColonToken && token.Parent.HasAnnotation(SpecializedFormattingAnnotation); public override AdjustNewLinesOperation GetAdjustNewLinesOperation( in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation) diff --git a/src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/CSharpUseObjectInitializerCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/CSharpUseObjectInitializerCodeFixProvider.cs index 8ae299d393040..7a2364acc67c5 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/CSharpUseObjectInitializerCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/CSharpUseObjectInitializerCodeFixProvider.cs @@ -13,6 +13,8 @@ namespace Microsoft.CodeAnalysis.CSharp.UseObjectInitializer { + using ObjectInitializerMatch = Match; + [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseObjectInitializer), Shared] internal class CSharpUseObjectInitializerCodeFixProvider : AbstractUseObjectInitializerCodeFixProvider< @@ -32,7 +34,7 @@ public CSharpUseObjectInitializerCodeFixProvider() protected override StatementSyntax GetNewStatement( StatementSyntax statement, BaseObjectCreationExpressionSyntax objectCreation, - ImmutableArray> matches) + ImmutableArray matches) { return statement.ReplaceNode( objectCreation, @@ -41,19 +43,20 @@ protected override StatementSyntax GetNewStatement( private static BaseObjectCreationExpressionSyntax GetNewObjectCreation( BaseObjectCreationExpressionSyntax objectCreation, - ImmutableArray> matches) + ImmutableArray matches) { return UseInitializerHelpers.GetNewObjectCreation( objectCreation, CreateExpressions(objectCreation, matches)); } private static SeparatedSyntaxList CreateExpressions( - BaseObjectCreationExpressionSyntax objectCreation, - ImmutableArray> matches) + BaseObjectCreationExpressionSyntax objectCreation, + ImmutableArray matches) { using var _ = ArrayBuilder.GetInstance(out var nodesAndTokens); - UseInitializerHelpers.AddExistingItems(objectCreation, nodesAndTokens); + UseInitializerHelpers.AddExistingItems( + objectCreation, nodesAndTokens, static (_, e) => e); for (var i = 0; i < matches.Length; i++) { diff --git a/src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/UseInitializerHelpers.cs b/src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/UseInitializerHelpers.cs index 6e0cef2918db2..727f661e6c596 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/UseInitializerHelpers.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseObjectInitializer/UseInitializerHelpers.cs @@ -2,6 +2,7 @@ // 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 Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; @@ -15,8 +16,7 @@ public static BaseObjectCreationExpressionSyntax GetNewObjectCreation( BaseObjectCreationExpressionSyntax baseObjectCreation, SeparatedSyntaxList expressions) { - if (baseObjectCreation is ObjectCreationExpressionSyntax objectCreation && - objectCreation.ArgumentList?.Arguments.Count == 0) + if (baseObjectCreation is ObjectCreationExpressionSyntax { ArgumentList.Arguments.Count: 0 } objectCreation) { baseObjectCreation = objectCreation .WithType(objectCreation.Type.WithTrailingTrivia(objectCreation.ArgumentList.GetTrailingTrivia())) @@ -31,10 +31,23 @@ public static BaseObjectCreationExpressionSyntax GetNewObjectCreation( return baseObjectCreation.WithInitializer(InitializerExpression(initializerKind, expressions)); } - public static void AddExistingItems(BaseObjectCreationExpressionSyntax objectCreation, ArrayBuilder nodesAndTokens) + public static void AddExistingItems( + BaseObjectCreationExpressionSyntax objectCreation, + ArrayBuilder nodesAndTokens, + Func createElement) + where TMatch : struct + where TElementSyntax : SyntaxNode { if (objectCreation.Initializer != null) - nodesAndTokens.AddRange(objectCreation.Initializer.Expressions.GetWithSeparators()); + { + foreach (var nodeOrToken in objectCreation.Initializer.Expressions.GetWithSeparators()) + { + if (nodeOrToken.IsToken) + nodesAndTokens.Add(nodeOrToken.AsToken()); + else + nodesAndTokens.Add(createElement(null, (ExpressionSyntax)nodeOrToken.AsNode()!)); + } + } // If we have an odd number of elements already, add a comma at the end so that we can add the rest of the // items afterwards without a syntax issue. diff --git a/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems b/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems index 45695b2a2dba2..a3b353293227f 100644 --- a/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems +++ b/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems @@ -99,6 +99,7 @@ + diff --git a/src/Analyzers/CSharp/Tests/UseCollectionExpression/UseCollectionExpressionForArrayTests.cs b/src/Analyzers/CSharp/Tests/UseCollectionExpression/UseCollectionExpressionForArrayTests.cs index 21f3e1cfcfb93..4ff0621a3174c 100644 --- a/src/Analyzers/CSharp/Tests/UseCollectionExpression/UseCollectionExpressionForArrayTests.cs +++ b/src/Analyzers/CSharp/Tests/UseCollectionExpression/UseCollectionExpressionForArrayTests.cs @@ -483,13 +483,6 @@ void M() var i = new int[] { 1 }.AsSpan(); } } - - //internal static class Extensions - //{ - // public static ReadOnlySpan AsSpan(this T[] values) => default; - //} - - //internal readonly struct ReadOnlySpan { } """, LanguageVersion = LanguageVersionExtensions.CSharpNext, ReferenceAssemblies = ReferenceAssemblies.Net.Net70, @@ -508,11 +501,11 @@ class C } """, FixedCode = """ - class C - { - private int[] X = [1]; - } - """, + class C + { + private int[] X = [1]; + } + """, LanguageVersion = LanguageVersionExtensions.CSharpNext, }.RunAsync(); } @@ -529,11 +522,11 @@ class C } """, FixedCode = """ - class C - { - private int[] X { get; } = [1]; - } - """, + class C + { + private int[] X { get; } = [1]; + } + """, LanguageVersion = LanguageVersionExtensions.CSharpNext, }.RunAsync(); } @@ -553,14 +546,14 @@ void M() } """, FixedCode = """ - class C - { - void M() + class C { - var c = (int[])[1]; + void M() + { + var c = (int[])[1]; + } } - } - """, + """, LanguageVersion = LanguageVersionExtensions.CSharpNext, }.RunAsync(); } @@ -667,7 +660,7 @@ void M(int[] x) } [Fact] - public async Task TestTargetTypedInConditional4() + public async Task TestNotTargetTypedInConditional4() { await new VerifyCS.Test { @@ -766,7 +759,7 @@ void M(int[] x, bool b) } [Fact] - public async Task TestTargetTypedInSwitchExpressionArm4() + public async Task TestNotTargetTypedInSwitchExpressionArm4() { await new VerifyCS.Test { @@ -784,7 +777,7 @@ void M(int[] x, bool b) } [Fact] - public async Task TestTargetTypedInitializer1() + public async Task TestNotTargetTypedInitializer1() { await new VerifyCS.Test { @@ -932,7 +925,7 @@ void X(int[] x) { } } [Fact] - public async Task TestTargetTypedArgument2() + public async Task TestNotTargetTypedArgument2() { await new VerifyCS.Test { @@ -1112,7 +1105,7 @@ void M(int[] x) } [Fact] - public async Task TestLinqLet() + public async Task TestNotWithLinqLet() { await new VerifyCS.Test { diff --git a/src/Analyzers/CSharp/Tests/UseCollectionInitializer/UseCollectionInitializerTests.cs b/src/Analyzers/CSharp/Tests/UseCollectionInitializer/UseCollectionInitializerTests.cs index 3903e99d70a67..a96d2931126fa 100644 --- a/src/Analyzers/CSharp/Tests/UseCollectionInitializer/UseCollectionInitializerTests.cs +++ b/src/Analyzers/CSharp/Tests/UseCollectionInitializer/UseCollectionInitializerTests.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.UseCollectionInitializer; using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; @@ -20,16 +21,21 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.UseCollectionInitialize [Trait(Traits.Feature, Traits.Features.CodeActionsUseCollectionInitializer)] public partial class UseCollectionInitializerTests { - private static async Task TestInRegularAndScriptAsync(string testCode, string fixedCode, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary) + private static async Task TestInRegularAndScriptAsync( + string testCode, + string fixedCode, + OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary) { - await new VerifyCS.Test + var test = new VerifyCS.Test { ReferenceAssemblies = Testing.ReferenceAssemblies.NetCore.NetCoreApp31, TestCode = testCode, FixedCode = fixedCode, - LanguageVersion = LanguageVersion.Preview, - TestState = { OutputKind = outputKind } - }.RunAsync(); + LanguageVersion = LanguageVersion.CSharp11, + TestState = { OutputKind = outputKind }, + }; + + await test.RunAsync(); } private static async Task TestMissingInRegularAndScriptAsync(string testCode, LanguageVersion? languageVersion = null) @@ -78,6 +84,76 @@ void M() """); } + [Fact] + public async Task TestOnVariableDeclarator_AddRange() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + var c = [|new|] List(); + [|c.Add(|]1); + c.AddRange(x); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + var c = new List + { + 1 + }; + c.AddRange(x); + } + } + """); + } + + [Fact] + public async Task TestOnVariableDeclarator_Foreach() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + var c = [|new|] List(); + [|c.Add(|]1); + foreach (var v in x) + c.Add(v); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + var c = new List + { + 1 + }; + foreach (var v in x) + c.Add(v); + } + } + """); + } + [Fact] public async Task TestIndexAccess1() { @@ -1437,7 +1513,10 @@ await TestInRegularAndScriptAsync( """ using System.Collections.Generic; - var list = new List { 1 }; + var list = new List + { + 1 + }; """, OutputKind.ConsoleApplication); } diff --git a/src/Analyzers/CSharp/Tests/UseCollectionInitializer/UseCollectionInitializerTests_CollectionExpression.cs b/src/Analyzers/CSharp/Tests/UseCollectionInitializer/UseCollectionInitializerTests_CollectionExpression.cs new file mode 100644 index 0000000000000..d856a6b4e1933 --- /dev/null +++ b/src/Analyzers/CSharp/Tests/UseCollectionInitializer/UseCollectionInitializerTests_CollectionExpression.cs @@ -0,0 +1,1805 @@ +// 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; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.UseCollectionInitializer; +using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.UseCollectionInitializer; + +using VerifyCS = CSharpCodeFixVerifier< + CSharpUseCollectionInitializerDiagnosticAnalyzer, + CSharpUseCollectionInitializerCodeFixProvider>; + +[Trait(Traits.Feature, Traits.Features.CodeActionsUseCollectionInitializer)] +public partial class UseCollectionInitializerTests_CollectionExpression +{ + private static async Task TestInRegularAndScriptAsync(string testCode, string fixedCode, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary) + { + await new VerifyCS.Test + { + ReferenceAssemblies = Testing.ReferenceAssemblies.NetCore.NetCoreApp31, + TestCode = testCode, + FixedCode = fixedCode, + LanguageVersion = LanguageVersion.Preview, + TestState = { OutputKind = outputKind } + }.RunAsync(); + } + + private static Task TestMissingInRegularAndScriptAsync(string testCode) + => TestInRegularAndScriptAsync(testCode, testCode); + + [Fact] + public async Task TestNotOnVarVariableDeclarator() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + var c = [|new|] List(); + [|c.Add(|]1); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M() + { + var c = new List + { + 1 + }; + } + } + """); + } + + [Fact] + public async Task TestOnVariableDeclarator() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = [|new|] List(); + [|c.Add(|]1); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = [1]; + } + } + """); + } + + [Fact] + public async Task TestOnVariableDeclaratorDifferentType() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + IList c = [|new|] List(); + [|c.Add(|]1); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M() + { + IList c = new List + { + 1 + }; + } + } + """); + } + + [Fact] + public async Task TestOnVariableDeclarator_Foreach1() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + List c = [|new|] List(); + [|c.Add(|]1); + [|foreach (var v in |]x) + c.Add(v); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + List c = [1, .. x]; + } + } + """); + } + + [Fact] + public async Task TestOnVariableDeclarator_Foreach1_A() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + List c = [|new|] List(); + [|c.Add(|]1); + [|foreach (var v in |]x) + { + c.Add(v); + } + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + List c = [1, .. x]; + } + } + """); + } + + [Fact] + public async Task TestOnVariableDeclarator_Foreach1_B() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + List c = [|new|] List(); + [|c.Add(|]1); + foreach (var v in x) + { + c.Add(0); + } + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + List c = [1]; + foreach (var v in x) + { + c.Add(0); + } + } + } + """); + } + + [Fact] + public async Task TestOnVariableDeclarator_Foreach1_C() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int[] x, int z) + { + List c = [|new|] List(); + [|c.Add(|]1); + foreach (var v in x) + { + c.Add(z); + } + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(int[] x, int z) + { + List c = [1]; + foreach (var v in x) + { + c.Add(z); + } + } + } + """); + } + + [Fact] + public async Task TestOnVariableDeclarator_Foreach2() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int[] x, int[] y) + { + List c = [|new|] List(); + [|c.Add(|]1); + [|foreach (var v in |]x) + c.Add(v); + [|foreach (var v in |]y) + c.Add(v); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(int[] x, int[] y) + { + List c = [1, .. x, .. y]; + } + } + """); + } + + [Fact] + public async Task TestOnVariableDeclarator_Foreach3() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int[] x, int[] y) + { + List c = [|new|] List(); + [|foreach (var v in |]x) + c.Add(v); + [|c.Add(|]1); + [|foreach (var v in |]y) + c.Add(v); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(int[] x, int[] y) + { + List c = [.. x, 1, .. y]; + } + } + """); + } + + [Fact] + public async Task TestOnVariableDeclarator_Foreach4() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int[] x, int[] y) + { + List c = [|new|] List(); + [|foreach (var v in |]x) + c.Add(v); + [|foreach (var v in |]y) + c.Add(v); + [|c.Add(|]1); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(int[] x, int[] y) + { + List c = [.. x, .. y, 1]; + } + } + """); + } + + [Fact] + public async Task TestOnVariableDeclarator_AddRange1() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + List c = [|new|] List(); + [|c.Add(|]1); + [|c.AddRange(|]x); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + List c = [1, .. x]; + } + } + """); + } + + [Fact] + public async Task TestOnVariableDeclarator_AddRangeAndForeach1() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int[] x, int[] y) + { + List c = [|new|] List(); + [|c.Add(|]1); + [|foreach (var v in |]x) + c.Add(v); + [|c.AddRange(|]y); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(int[] x, int[] y) + { + List c = [1, .. x, .. y]; + } + } + """); + } + + [Fact] + public async Task TestIndexAccess1() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + class C + { + void M() + { + List c = [|new|] List(); + c[1] = 2; + } + } + """, + """ + using System.Collections.Generic; + class C + { + void M() + { + List c = new List + { + [1] = 2 + }; + } + } + """); + } + + [Fact] + public async Task TestIndexAccess1_Foreach() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + class C + { + void M(int[] x) + { + List c = [|new|] List(); + c[1] = 2; + foreach (var v in x) + c.Add(v); + } + } + """, + """ + using System.Collections.Generic; + class C + { + void M(int[] x) + { + List c = new List + { + [1] = 2 + }; + foreach (var v in x) + c.Add(v); + } + } + """); + } + + [Fact] + public async Task TestComplexIndexAccess1() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class A + { + public B b; + } + + class B + { + public List c; + } + + class C + { + void M(A a) + { + a.b.c = [|new|] List(); + a.b.c[1] = 2; + } + } + """, + """ + using System.Collections.Generic; + + class A + { + public B b; + } + + class B + { + public List c; + } + + class C + { + void M(A a) + { + a.b.c = new List + { + [1] = 2 + }; + } + } + """); + } + + [Fact] + public async Task TestIndexAccess2() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + class C + { + void M() + { + List c = [|new|] List(); + c[1] = 2; + c[2] = ""; + } + } + """, + """ + using System.Collections.Generic; + class C + { + void M() + { + List c = new List + { + [1] = 2, + [2] = "" + }; + } + } + """); + } + + [Fact] + public async Task TestIndexAccess3() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections; + + class C + { + void M() + { + X c = [|new|] X(); + c[1] = 2; + c[2] = ""; + c[3, 4] = 5; + } + } + + class X : IEnumerable + { + public object this[int i] { get => null; set { } } + public object this[int i, int j] { get => null; set { } } + + public IEnumerator GetEnumerator() => null; + public void Add(int i) { } + } + """, + """ + using System.Collections; + + class C + { + void M() + { + X c = new X + { + [1] = 2, + [2] = "", + [3, 4] = 5 + }; + } + } + + class X : IEnumerable + { + public object this[int i] { get => null; set { } } + public object this[int i, int j] { get => null; set { } } + + public IEnumerator GetEnumerator() => null; + public void Add(int i) { } + } + """); + } + + [Fact] + public async Task TestIndexFollowedByInvocation() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + class C + { + void M() + { + List c = [|new|] List(); + c[1] = 2; + c.Add(0); + } + } + """, + """ + using System.Collections.Generic; + class C + { + void M() + { + List c = new List + { + [1] = 2 + }; + c.Add(0); + } + } + """); + } + + [Fact] + public async Task TestInvocationFollowedByIndex() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + class C + { + void M() + { + List c = [|new|] List(); + [|c.Add(|]0); + c[1] = 2; + } + } + """, + """ + using System.Collections.Generic; + class C + { + void M() + { + List c = [0]; + c[1] = 2; + } + } + """); + } + + [Fact] + public async Task TestWithInterimStatement() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = [|new|] List(); + [|c.Add(|]1); + [|c.Add(|]2); + throw new System.Exception(); + c.Add(3); + c.Add(4); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = [1, 2]; + throw new System.Exception(); + c.Add(3); + c.Add(4); + } + } + """); + } + + [Fact] + public async Task TestMissingOnNonIEnumerable() + { + await TestMissingInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + C c = new C(); + c.Add(1); + } + + void Add(int i) { } + } + """); + } + + [Fact] + public async Task TestMissingOnNonIEnumerableEvenWithAdd() + { + await TestMissingInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + C c = new C(); + c.Add(1); + } + + public void Add(int i) + { + } + } + """); + } + + [Fact] + public async Task TestWithCreationArguments() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = [|new|] List(1); + [|c.Add(|]1); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = new List(1) + { + 1 + }; + } + } + """); + } + + [Fact] + public async Task TestOnAssignmentExpression() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = null; + c = [|new|] List(); + [|c.Add(|]1); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = null; + c = [1]; + } + } + """); + } + + [Fact] + public async Task TestMissingOnRefAdd() + { + await TestMissingInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int i) + { + List c = new List(); + c.Add(ref i); + } + } + + + class List + { + public void Add(ref int i) + { + } + } + """); + } + + [Fact] + public async Task TestComplexInitializer() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(List[] array) + { + array[0] = [|new|] List(); + [|array[0].Add(|]1); + [|array[0].Add(|]2); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(List[] array) + { + array[0] = [1, 2]; + } + } + """); + } + + [Fact] + public async Task TestNotOnNamedArg() + { + await TestMissingInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = new List(); + c.Add(item: 1); + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/39146")] + public async Task TestWithExistingInitializer() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = [|new|] List() + { + 1 + }; + [|c.Add(|]2); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = [1, + 2]; + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/39146")] + public async Task TestWithExistingInitializer_NoParens() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = [|new|] List + { + 1 + }; + [|c.Add(|]2); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = [1, + 2]; + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/39146")] + public async Task TestWithExistingInitializerWithComma() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = [|new|] List() + { + 1, + }; + [|c.Add(|]2); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M() + { + List c = [1, + 2]; + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/39146")] + public async Task TestWithExistingInitializer2() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + List c = [|new|] List() + { + 1 + }; + [|foreach (var y in |]x) + c.Add(y); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(int[] x) + { + List c = [1, + .. x]; + } + } + """); + } + + [Fact] + public async Task TestFixAllInDocument1() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M(List[] array) + { + array[0] = [|new|] List(); + [|array[0].Add(|]1); + [|array[0].Add(|]2); + array[1] = [|new|] List(); + [|array[1].Add(|]3); + [|array[1].Add(|]4); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M(List[] array) + { + array[0] = [1, 2]; + array[1] = [3, 4]; + } + } + """); + } + + [Fact] + public async Task TestFixAllInDocument2() + { + await TestInRegularAndScriptAsync( + """ + using System; + using System.Collections; + using System.Collections.Generic; + + class C + { + void M() + { + Bar list1 = [|new|] Bar(() => { + List list2 = [|new|] List(); + [|list2.Add(|]2); + }); + [|list1.Add(|]1); + } + } + + class Bar : IEnumerable + { + public Bar(Action action) { } + + public IEnumerator GetEnumerator() => null; + public void Add(int i) { } + } + """, + """ + using System; + using System.Collections; + using System.Collections.Generic; + + class C + { + void M() + { + Bar list1 = new Bar(() => + { + List list2 = [2]; + }) + { + 1 + }; + } + } + + class Bar : IEnumerable + { + public Bar(Action action) { } + + public IEnumerator GetEnumerator() => null; + public void Add(int i) { } + } + """); + } + + [Fact] + public async Task TestFixAllInDocument3() + { + await new VerifyCS.Test + { + TestCode = + """ + using System; + using System.Collections.Generic; + + class C + { + void M() + { + List list1 = [|new|] List(); + [|list1.Add(|]() => { + List list2 = [|new|] List(); + [|list2.Add(|]2); + }); + } + } + """, + FixedCode = + """ + using System; + using System.Collections.Generic; + + class C + { + void M() + { + List list1 = [() => + { + List list2 = [2]; + } + + ]; + } + } + """, + BatchFixedCode = + """ + using System; + using System.Collections.Generic; + + class C + { + void M() + { + List list1 = [() => + { + List list2 = [2]; + } + + ]; + } + } + """, + LanguageVersion = LanguageVersion.Preview, + }.RunAsync(); + } + + [Fact] + public async Task TestTrivia1() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + class C + { + void M() + { + List c = [|new|] List(); + [|c.Add(|]1); // Goo + [|c.Add(|]2); // Bar + } + } + """, + """ + using System.Collections.Generic; + class C + { + void M() + { + List c = [1, // Goo + 2 // Bar + ]; + } + } + """); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseObjectInitializer)] + [WorkItem("https://github.com/dotnet/roslyn/issues/46670")] + public async Task TestTriviaRemoveLeadingBlankLinesForFirstElement() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + class C + { + void M() + { + List c = [|new|] List(); + + // Goo + [|c.Add(|]1); + + // Bar + [|c.Add(|]2); + } + } + """, + """ + using System.Collections.Generic; + class C + { + void M() + { + List c = [ + // Goo + 1, + // Bar + 2]; + } + } + """); + } + + [Fact] + public async Task TestComplexInitializer2() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + void M() + { + Dictionary c = [|new|] Dictionary(); + [|c.Add(|]1, "x"); + [|c.Add(|]2, "y"); + } + } + """, + """ + using System.Collections.Generic; + + class C + { + void M() + { + Dictionary c = new Dictionary + { + { 1, "x" }, + { 2, "y" } + }; + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/16158")] + public async Task TestIncorrectAddName() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + public class Goo + { + public static void Bar() + { + string item = null; + var items = new List(); + + List values = [|new|] List(); // Collection initialization can be simplified + [|values.Add(|]item); + values.Remove(item); + } + } + """, + """ + using System.Collections.Generic; + + public class Goo + { + public static void Bar() + { + string item = null; + var items = new List(); + + List values = [item]; // Collection initialization can be simplified + values.Remove(item); + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/16241")] + public async Task TestNestedCollectionInitializer() + { + await TestMissingInRegularAndScriptAsync( + """ + using System.Collections.Generic; + using System.Linq; + + class Program + { + static void Main(string[] args) + { + string[] myStringArray = new string[] { "Test", "123", "ABC" }; + List myStringList = myStringArray?.ToList() ?? new List(); + myStringList.Add("Done"); + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/17823")] + public async Task TestMissingWhenReferencedInInitializer() + { + await TestMissingInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + static void M() + { + List items = new List(); + items[0] = items[0]; + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/17823")] + public async Task TestWhenReferencedInInitializer_LocalVar() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + static void M() + { + List items = [|new|] List(); + items[0] = 1; + items[1] = items[0]; + } + } + """, + """ + using System.Collections.Generic; + + class C + { + static void M() + { + List items = [|new|] List + { + [0] = 1 + }; + items[1] = items[0]; + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/17823")] + public async Task TestWhenReferencedInInitializer_LocalVar2() + { + await TestMissingInRegularAndScriptAsync( + """ + using System.Collections.Generic; + using System.Linq; + + class C + { + void M() + { + List t = new List(new int[] { 1, 2, 3 }); + t.Add(t.Min() - 1); + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/18260")] + public async Task TestWhenReferencedInInitializer_Assignment() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + static void M() + { + List items = null; + items = [|new|] List(); + items[0] = 1; + items[1] = items[0]; + } + } + """, + """ + using System.Collections.Generic; + + class C + { + static void M() + { + List items = null; + items = [|new|] List + { + [0] = 1 + }; + items[1] = items[0]; + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/18260")] + public async Task TestWhenReferencedInInitializer_Assignment2() + { + await TestMissingInRegularAndScriptAsync( + """ + using System.Collections.Generic; + using System.Linq; + + class C + { + void M() + { + List t = null; + t = new List(new int[] { 1, 2, 3 }); + t.Add(t.Min() - 1); + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/18260")] + public async Task TestFieldReference() + { + await TestMissingInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + private List myField; + void M() + { + myField = new List(); + myField.Add(this.myField.Count); + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/17853")] + public async Task TestMissingForDynamic() + { + await TestMissingInRegularAndScriptAsync( + """ + using System.Dynamic; + + class C + { + void Goo() + { + dynamic body = new ExpandoObject(); + body[0] = new ExpandoObject(); + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/17953")] + public async Task TestMissingAcrossPreprocessorDirective() + { + await TestMissingInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + public class Goo + { + public void M() + { + List items = new List(); + #if true + items.Add(1); + #endif + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/17953")] + public async Task TestAvailableInsidePreprocessorDirective() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + public class Goo + { + public void M() + { + #if true + List items = [|new|] List(); + [|items.Add(|]1); + #endif + } + } + """, + """ + using System.Collections.Generic; + + public class Goo + { + public void M() + { + #if true + List items = [1]; + #endif + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/18242")] + public async Task TestObjectInitializerAssignmentAmbiguity() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + public class Goo + { + public void M() + { + int lastItem; + List list = [|new|] List(); + [|list.Add(|]lastItem = 5); + } + } + """, + """ + using System.Collections.Generic; + + public class Goo + { + public void M() + { + int lastItem; + List list = [(lastItem = 5)]; + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/18242")] + public async Task TestObjectInitializerCompoundAssignment() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + public class Goo + { + public void M() + { + int lastItem = 0; + List list = [|new|] List(); + [|list.Add(|]lastItem += 5); + } + } + """, + """ + using System.Collections.Generic; + + public class Goo + { + public void M() + { + int lastItem = 0; + List list = [(lastItem += 5)]; + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/19253")] + public async Task TestKeepBlankLinesAfter() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class MyClass + { + public void Main() + { + List list = [|new|] List(); + [|list.Add(|]1); + + int horse = 1; + } + } + """, + """ + using System.Collections.Generic; + + class MyClass + { + public void Main() + { + List list = [1]; + + int horse = 1; + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/23672")] + public async Task TestMissingWithExplicitImplementedAddMethod() + { + await TestMissingInRegularAndScriptAsync( + """ + using System.Collections.Generic; + using System.Dynamic; + + public class Goo + { + public void M() + { + IDictionary obj = new ExpandoObject(); + obj.Add("string", "v"); + obj.Add("int", 1); + obj.Add(" object", new { X = 1, Y = 2 }); + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/47632")] + public async Task TestWhenReferencedInInitializerLeft() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + static void M() + { + List items = [|new|] List(); + items[0] = 1; + items[items.Count - 1] = 2; + } + } + """, + """ + using System.Collections.Generic; + + class C + { + static void M() + { + List items = [|new|] List + { + [0] = 1 + }; + items[items.Count - 1] = 2; + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/47632")] + public async Task TestWithIndexerInInitializerLeft() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + static void M() + { + List items = [|new|] List(); + items[0] = 1; + items[^1] = 2; + } + } + """, + """ + using System.Collections.Generic; + + class C + { + static void M() + { + List items = new List + { + [0] = 1 + }; + items[^1] = 2; + } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/47632")] + public async Task TestWithImplicitObjectCreation() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + class C + { + static void M() + { + List items = [|new|](); + items[0] = 1; + } + } + """, + """ + using System.Collections.Generic; + + class C + { + static void M() + { + List items = new() + { + [0] = 1 + }; + } + } + """); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseObjectInitializer)] + [WorkItem("https://github.com/dotnet/roslyn/issues/61066")] + public async Task TestInTopLevelStatements() + { + await TestInRegularAndScriptAsync( + """ + using System.Collections.Generic; + + List list = [|new|] List(); + [|list.Add(|]1); + """, + """ + using System.Collections.Generic; + + List list = [1]; + + """, OutputKind.ConsoleApplication); + } +} diff --git a/src/Analyzers/Core/Analyzers/Analyzers.projitems b/src/Analyzers/Core/Analyzers/Analyzers.projitems index 8e2fb4f89b1ae..1c12ab9611ae8 100644 --- a/src/Analyzers/Core/Analyzers/Analyzers.projitems +++ b/src/Analyzers/Core/Analyzers/Analyzers.projitems @@ -83,6 +83,7 @@ + diff --git a/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractObjectCreationExpressionAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractObjectCreationExpressionAnalyzer.cs index 867f1030861f6..6317b8d68204b 100644 --- a/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractObjectCreationExpressionAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractObjectCreationExpressionAnalyzer.cs @@ -27,6 +27,7 @@ internal abstract class AbstractObjectCreationExpressionAnalyzer< protected SemanticModel _semanticModel; protected ISyntaxFacts _syntaxFacts; protected TObjectCreationExpressionSyntax _objectCreationExpression; + protected bool _analyzeForCollectionExpression; protected CancellationToken _cancellationToken; protected TStatementSyntax _containingStatement; @@ -41,11 +42,13 @@ public void Initialize( SemanticModel semanticModel, ISyntaxFacts syntaxFacts, TObjectCreationExpressionSyntax objectCreationExpression, + bool analyzeForCollectionExpression, CancellationToken cancellationToken) { _semanticModel = semanticModel; _syntaxFacts = syntaxFacts; _objectCreationExpression = objectCreationExpression; + _analyzeForCollectionExpression = analyzeForCollectionExpression; _cancellationToken = cancellationToken; } @@ -54,6 +57,7 @@ protected void Clear() _semanticModel = null; _syntaxFacts = null; _objectCreationExpression = null; + _analyzeForCollectionExpression = false; _cancellationToken = default; _containingStatement = null; _valuePattern = default; @@ -63,19 +67,19 @@ protected void Clear() protected abstract bool ShouldAnalyze(); protected abstract void AddMatches(ArrayBuilder matches); - protected ImmutableArray? AnalyzeWorker() + protected ImmutableArray AnalyzeWorker() { if (!ShouldAnalyze()) - return null; + return default; _containingStatement = _objectCreationExpression.FirstAncestorOrSelf(); if (_containingStatement == null) - return null; + return default; if (!TryInitializeVariableDeclarationCase() && !TryInitializeAssignmentCase()) { - return null; + return default; } using var _ = ArrayBuilder.GetInstance(out var matches); diff --git a/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs index 894eb02a1edff..8391c8dddefbf 100644 --- a/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs @@ -2,8 +2,12 @@ // 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; using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis.Analyzers.UseCollectionInitializer; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageService; @@ -13,6 +17,10 @@ namespace Microsoft.CodeAnalysis.UseCollectionInitializer { + internal readonly record struct Match( + TStatementSyntax Statement, + bool UseSpread) where TStatementSyntax : SyntaxNode; + internal abstract partial class AbstractUseCollectionInitializerDiagnosticAnalyzer< TSyntaxKind, TExpressionSyntax, @@ -21,6 +29,7 @@ internal abstract partial class AbstractUseCollectionInitializerDiagnosticAnalyz TMemberAccessExpressionSyntax, TInvocationExpressionSyntax, TExpressionStatementSyntax, + TForeachStatementSyntax, TVariableDeclaratorSyntax> : AbstractBuiltInCodeStyleDiagnosticAnalyzer where TSyntaxKind : struct @@ -30,8 +39,10 @@ internal abstract partial class AbstractUseCollectionInitializerDiagnosticAnalyz where TMemberAccessExpressionSyntax : TExpressionSyntax where TInvocationExpressionSyntax : TExpressionSyntax where TExpressionStatementSyntax : TStatementSyntax + where TForeachStatementSyntax : TStatementSyntax where TVariableDeclaratorSyntax : SyntaxNode { + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; @@ -57,9 +68,12 @@ protected AbstractUseCollectionInitializerDiagnosticAnalyzer() } protected abstract ISyntaxFacts GetSyntaxFacts(); + protected abstract bool AreCollectionInitializersSupported(Compilation compilation); + protected abstract bool AreCollectionExpressionsSupported(Compilation compilation); + protected abstract bool CanUseCollectionExpression(SemanticModel semanticModel, TObjectCreationExpressionSyntax objectCreationExpression, CancellationToken cancellationToken); - protected override void InitializeWorker(AnalysisContext context) + protected sealed override void InitializeWorker(AnalysisContext context) => context.RegisterCompilationStartAction(OnCompilationStart); private void OnCompilationStart(CompilationStartAnalysisContext context) @@ -104,23 +118,21 @@ private void AnalyzeNode(SyntaxNodeAnalysisContext context, INamedTypeSymbol ien return; } - // Object creation can only be converted to collection initializer if it - // implements the IEnumerable type. + // Object creation can only be converted to collection initializer if it implements the IEnumerable type. var objectType = context.SemanticModel.GetTypeInfo(objectCreationExpression, cancellationToken); if (objectType.Type == null || !objectType.Type.AllInterfaces.Contains(ienumerableType)) return; - var matches = UseCollectionInitializerAnalyzer.Analyze( - semanticModel, GetSyntaxFacts(), objectCreationExpression, cancellationToken); - - if (matches == null || matches.Value.Length == 0) - return; - var containingStatement = objectCreationExpression.FirstAncestorOrSelf(); if (containingStatement == null) return; - var nodes = ImmutableArray.Create(containingStatement).AddRange(matches.Value); + var (matches, shouldUseCollectionExpression) = GetMatches(); + // If we got no matches, then we def can't convert this. + if (matches.IsDefaultOrEmpty) + return; + + var nodes = ImmutableArray.Create(containingStatement).AddRange(matches.Select(static m => m.Statement)); var syntaxFacts = GetSyntaxFacts(); if (syntaxFacts.ContainsInterleavedDirective(nodes, cancellationToken)) return; @@ -132,37 +144,100 @@ private void AnalyzeNode(SyntaxNodeAnalysisContext context, INamedTypeSymbol ien objectCreationExpression.GetFirstToken().GetLocation(), option.Notification.Severity, additionalLocations: locations, - properties: null)); + properties: shouldUseCollectionExpression ? UseCollectionInitializerHelpers.UseCollectionExpressionProperties : null)); + + FadeOutCode(context, matches, locations); - FadeOutCode(context, matches.Value, locations); + return; + + (ImmutableArray> matches, bool shouldUseCollectionExpression) GetMatches() + { + // Analyze the surrounding statements. First, try a broader set of statements if the language supports + // collection expressions. + var analyzeForCollectionExpression = AreCollectionExpressionsSupported(); + var matches = UseCollectionInitializerAnalyzer< + TExpressionSyntax, TStatementSyntax, TObjectCreationExpressionSyntax, TMemberAccessExpressionSyntax, TInvocationExpressionSyntax, TExpressionStatementSyntax, TForeachStatementSyntax, TVariableDeclaratorSyntax>.Analyze( + semanticModel, GetSyntaxFacts(), objectCreationExpression, analyzeForCollectionExpression, cancellationToken); + + // if this was a normal (non-collection-expr) analysis, then just return what we got. + if (!analyzeForCollectionExpression) + return (matches, shouldUseCollectionExpression: false); + + // If we succeeded in finding matches, and this is a location a collection expression is legal in, then convert to that. + if (!matches.IsDefaultOrEmpty && CanUseCollectionExpression(semanticModel, objectCreationExpression, cancellationToken)) + return (matches, analyzeForCollectionExpression); + + // we tried collection expression, and were not successful. try again, this time without collection exprs. + analyzeForCollectionExpression = false; + matches = UseCollectionInitializerAnalyzer< + TExpressionSyntax, TStatementSyntax, TObjectCreationExpressionSyntax, TMemberAccessExpressionSyntax, TInvocationExpressionSyntax, TExpressionStatementSyntax, TForeachStatementSyntax, TVariableDeclaratorSyntax>.Analyze( + semanticModel, GetSyntaxFacts(), objectCreationExpression, analyzeForCollectionExpression, cancellationToken); + return (matches, analyzeForCollectionExpression); + } + + bool AreCollectionExpressionsSupported() + { + if (!this.AreCollectionExpressionsSupported(context.Compilation)) + return false; + + var option = context.GetAnalyzerOptions().PreferCollectionExpression; + if (!option.Value) + return false; + + var syntaxFacts = GetSyntaxFacts(); + var arguments = syntaxFacts.GetArgumentsOfObjectCreationExpression(objectCreationExpression); + if (arguments.Count != 0) + return false; + + return true; + } } private void FadeOutCode( SyntaxNodeAnalysisContext context, - ImmutableArray matches, + ImmutableArray> matches, ImmutableArray locations) { var syntaxTree = context.Node.SyntaxTree; var syntaxFacts = GetSyntaxFacts(); - foreach (var match in matches) + foreach (var (match, _) in matches) { - var expression = syntaxFacts.GetExpressionOfExpressionStatement(match); - - if (syntaxFacts.IsInvocationExpression(expression)) + if (match is TExpressionStatementSyntax) + { + var expression = syntaxFacts.GetExpressionOfExpressionStatement(match); + + if (syntaxFacts.IsInvocationExpression(expression)) + { + var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(expression); + var additionalUnnecessaryLocations = ImmutableArray.Create( + syntaxTree.GetLocation(TextSpan.FromBounds(match.SpanStart, arguments[0].SpanStart)), + syntaxTree.GetLocation(TextSpan.FromBounds(arguments.Last().FullSpan.End, match.Span.End))); + + // Report the diagnostic at the first unnecessary location. This is the location where the code fix + // will be offered. + context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( + s_unnecessaryCodeDescriptor, + additionalUnnecessaryLocations[0], + ReportDiagnostic.Default, + additionalLocations: locations, + additionalUnnecessaryLocations: additionalUnnecessaryLocations)); + } + } + else if (match is TForeachStatementSyntax) { - var arguments = syntaxFacts.GetArgumentsOfInvocationExpression(expression); + // For a `foreach (var x in expr) ...` statement, fade out the parts before and after `expr`. + + var expression = syntaxFacts.GetExpressionOfForeachStatement(match); var additionalUnnecessaryLocations = ImmutableArray.Create( - syntaxTree.GetLocation(TextSpan.FromBounds(match.SpanStart, arguments[0].SpanStart)), - syntaxTree.GetLocation(TextSpan.FromBounds(arguments.Last().FullSpan.End, match.Span.End))); + syntaxTree.GetLocation(TextSpan.FromBounds(match.SpanStart, expression.SpanStart)), + syntaxTree.GetLocation(TextSpan.FromBounds(expression.FullSpan.End, match.Span.End))); // Report the diagnostic at the first unnecessary location. This is the location where the code fix // will be offered. - var location1 = additionalUnnecessaryLocations[0]; - context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( s_unnecessaryCodeDescriptor, - location1, + additionalUnnecessaryLocations[0], ReportDiagnostic.Default, additionalLocations: locations, additionalUnnecessaryLocations: additionalUnnecessaryLocations)); diff --git a/src/Analyzers/Core/Analyzers/UseCollectionInitializer/UseCollectionInitializerAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseCollectionInitializer/UseCollectionInitializerAnalyzer.cs index d787670b7e51a..9a285a1824c25 100644 --- a/src/Analyzers/Core/Analyzers/UseCollectionInitializer/UseCollectionInitializerAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseCollectionInitializer/UseCollectionInitializerAnalyzer.cs @@ -2,6 +2,7 @@ // 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.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -12,38 +13,41 @@ namespace Microsoft.CodeAnalysis.UseCollectionInitializer { - internal class UseCollectionInitializerAnalyzer< + internal sealed class UseCollectionInitializerAnalyzer< TExpressionSyntax, TStatementSyntax, TObjectCreationExpressionSyntax, TMemberAccessExpressionSyntax, TInvocationExpressionSyntax, TExpressionStatementSyntax, + TForeachStatementSyntax, TVariableDeclaratorSyntax> : AbstractObjectCreationExpressionAnalyzer< TExpressionSyntax, TStatementSyntax, TObjectCreationExpressionSyntax, TVariableDeclaratorSyntax, - TExpressionStatementSyntax> + Match> where TExpressionSyntax : SyntaxNode where TStatementSyntax : SyntaxNode where TObjectCreationExpressionSyntax : TExpressionSyntax where TMemberAccessExpressionSyntax : TExpressionSyntax where TInvocationExpressionSyntax : TExpressionSyntax where TExpressionStatementSyntax : TStatementSyntax + where TForeachStatementSyntax : TStatementSyntax where TVariableDeclaratorSyntax : SyntaxNode { - private static readonly ObjectPool> s_pool - = SharedPools.Default>(); + private static readonly ObjectPool> s_pool + = SharedPools.Default>(); - public static ImmutableArray? Analyze( + public static ImmutableArray> Analyze( SemanticModel semanticModel, ISyntaxFacts syntaxFacts, TObjectCreationExpressionSyntax objectCreationExpression, + bool areCollectionExpressionsSupported, CancellationToken cancellationToken) { var analyzer = s_pool.Allocate(); - analyzer.Initialize(semanticModel, syntaxFacts, objectCreationExpression, cancellationToken); + analyzer.Initialize(semanticModel, syntaxFacts, objectCreationExpression, areCollectionExpressionsSupported, cancellationToken); try { return analyzer.AnalyzeWorker(); @@ -55,7 +59,7 @@ private static readonly ObjectPool matches) + protected override void AddMatches(ArrayBuilder> matches) { // If containing statement is inside a block (e.g. method), than we need to iterate through its child statements. // If containing statement is in top-level code, than we need to iterate through child statements of containing compilation unit. @@ -81,6 +85,10 @@ protected override void AddMatches(ArrayBuilder matc seenInvocation = !seenIndexAssignment; } + // An indexer can't be used with a collection expression. So fail out immediately if we see that. + if (seenIndexAssignment && _analyzeForCollectionExpression) + return; + foreach (var child in containingBlockOrCompilationUnit.ChildNodesAndTokens()) { if (child.IsToken) @@ -99,23 +107,86 @@ protected override void AddMatches(ArrayBuilder matc continue; } - if (extractedChild is not TExpressionStatementSyntax statement) - return; - - SyntaxNode? instance = null; - if (!seenIndexAssignment && TryAnalyzeAddInvocation(statement, out instance)) - seenInvocation = true; + if (extractedChild is TExpressionStatementSyntax expressionStatement) + { + if (!seenIndexAssignment) + { + // Look for a call to Add or AddRange + if (TryAnalyzeInvocation( + expressionStatement, + addName: WellKnownMemberNames.CollectionInitializerAddMethodName, + requiredArgumentName: null, + out var instance) && + ValuePatternMatches(instance)) + { + seenInvocation = true; + matches.Add(new Match(expressionStatement, UseSpread: false)); + continue; + } + else if ( + _analyzeForCollectionExpression && + TryAnalyzeInvocation( + expressionStatement, + addName: nameof(List.AddRange), + requiredArgumentName: null, + out instance)) + { + seenInvocation = true; + + // AddRange(x) will become `..x` when we make it into a collection expression. + matches.Add(new Match(expressionStatement, UseSpread: true)); + continue; + } + } - if (!seenInvocation && TryAnalyzeIndexAssignment(statement, out instance)) - seenIndexAssignment = true; + if (!seenInvocation && !_analyzeForCollectionExpression) + { + if (TryAnalyzeIndexAssignment(expressionStatement, out var instance)) + { + seenIndexAssignment = true; + matches.Add(new Match(expressionStatement, UseSpread: false)); + continue; + } + } - if (instance == null) return; + } + else if (extractedChild is TForeachStatementSyntax foreachStatement) + { + // if we're not producing a collection expression, then we cannot convert any foreach'es into + // `[..expr]` elements. + if (!_analyzeForCollectionExpression) + return; + + _syntaxFacts.GetPartsOfForeachStatement(foreachStatement, out var identifier, out _, out var foreachStatements); + if (identifier == default) + return; + + // must be of the form: + // + // foreach (var x in expr) + // dest.Add(x) + // + // By passing 'x' into TryAnalyzeInvocation below, we ensure that it is an enumerated value from `expr` + // being added to `dest`. + if (foreachStatements.ToImmutableArray() is not [TExpressionStatementSyntax childExpressionStatement] || + !TryAnalyzeInvocation( + childExpressionStatement, + addName: WellKnownMemberNames.CollectionInitializerAddMethodName, + requiredArgumentName: identifier.Text, + out var instance) || + !ValuePatternMatches(instance)) + { + return; + } - if (!ValuePatternMatches((TExpressionSyntax)instance)) + // `foreach` will become `..expr` when we make it into a collection expression. + matches.Add(new Match(foreachStatement, UseSpread: true)); + } + else + { return; - - matches.Add(statement); + } } } @@ -139,7 +210,7 @@ protected override bool ShouldAnalyze() private bool TryAnalyzeIndexAssignment( TExpressionStatementSyntax statement, - [NotNullWhen(true)] out SyntaxNode? instance) + [NotNullWhen(true)] out TExpressionSyntax? instance) { instance = null; if (!_syntaxFacts.SupportsIndexingInitializer(statement.SyntaxTree.Options)) @@ -176,13 +247,15 @@ private bool TryAnalyzeIndexAssignment( return false; } - instance = elementInstance; + instance = elementInstance as TExpressionSyntax; return instance != null; } - private bool TryAnalyzeAddInvocation( + private bool TryAnalyzeInvocation( TExpressionStatementSyntax statement, - [NotNullWhen(true)] out SyntaxNode? instance) + string addName, + string? requiredArgumentName, + [NotNullWhen(true)] out TExpressionSyntax? instance) { instance = null; if (_syntaxFacts.GetExpressionOfExpressionStatement(statement) is not TInvocationExpressionSyntax invocationExpression) @@ -192,6 +265,14 @@ private bool TryAnalyzeAddInvocation( if (arguments.Count < 1) return false; + // Collection expressions can only call the single argument Add/AddRange methods on a type. + // So if we don't have exactly one argument, fail out. + if (_analyzeForCollectionExpression && arguments.Count != 1) + return false; + + if (requiredArgumentName != null && arguments.Count != 1) + return false; + foreach (var argument in arguments) { if (!_syntaxFacts.IsSimpleArgument(argument)) @@ -210,6 +291,18 @@ private bool TryAnalyzeAddInvocation( // instead looks for an 3-argument `Add` method to invoke on `List` (which clearly fails). if (_syntaxFacts.SyntaxKinds.CollectionInitializerExpression == argumentExpression.RawKind) return false; + + // If the caller is requiring a particular argument name, then validate that is what this argument + // is referencing. + if (requiredArgumentName != null) + { + if (!_syntaxFacts.IsIdentifierName(argumentExpression)) + return false; + + _syntaxFacts.GetNameAndArityOfSimpleName(argumentExpression, out var suppliedName, out _); + if (requiredArgumentName != suppliedName) + return false; + } } if (_syntaxFacts.GetExpressionOfInvocationExpression(invocationExpression) is not TMemberAccessExpressionSyntax memberAccess) @@ -221,11 +314,11 @@ private bool TryAnalyzeAddInvocation( _syntaxFacts.GetPartsOfMemberAccessExpression(memberAccess, out var localInstance, out var memberName); _syntaxFacts.GetNameAndArityOfSimpleName(memberName, out var name, out var arity); - if (arity != 0 || !Equals(name, WellKnownMemberNames.CollectionInitializerAddMethodName)) + if (arity != 0 || !Equals(name, addName)) return false; - instance = localInstance; - return true; + instance = localInstance as TExpressionSyntax; + return instance != null; } } } diff --git a/src/Analyzers/Core/Analyzers/UseCollectionInitializer/UseCollectionInitializerHelpers.cs b/src/Analyzers/Core/Analyzers/UseCollectionInitializer/UseCollectionInitializerHelpers.cs new file mode 100644 index 0000000000000..2a7f5a6e26f35 --- /dev/null +++ b/src/Analyzers/Core/Analyzers/UseCollectionInitializer/UseCollectionInitializerHelpers.cs @@ -0,0 +1,18 @@ +// 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.Text; + +namespace Microsoft.CodeAnalysis.Analyzers.UseCollectionInitializer; + +internal static class UseCollectionInitializerHelpers +{ + public const string UseCollectionExpressionName = nameof(UseCollectionExpressionName); + + public static readonly ImmutableDictionary UseCollectionExpressionProperties = + ImmutableDictionary.Empty.Add(UseCollectionExpressionName, UseCollectionExpressionName); +} diff --git a/src/Analyzers/Core/Analyzers/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs index cd1fd19facd3f..37dbef903b579 100644 --- a/src/Analyzers/Core/Analyzers/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs @@ -98,7 +98,7 @@ private void AnalyzeNode(SyntaxNodeAnalysisContext context) var matches = UseNamedMemberInitializerAnalyzer.Analyze( context.SemanticModel, syntaxFacts, objectCreationExpression, context.CancellationToken); - if (matches == null || matches.Value.Length == 0) + if (matches.IsDefaultOrEmpty) return; var containingStatement = objectCreationExpression.FirstAncestorOrSelf(); @@ -108,7 +108,7 @@ private void AnalyzeNode(SyntaxNodeAnalysisContext context) if (!IsValidContainingStatement(containingStatement)) return; - var nodes = ImmutableArray.Create(containingStatement).AddRange(matches.Value.Select(m => m.Statement)); + var nodes = ImmutableArray.Create(containingStatement).AddRange(matches.Select(m => m.Statement)); if (syntaxFacts.ContainsInterleavedDirective(nodes, context.CancellationToken)) return; @@ -121,7 +121,7 @@ private void AnalyzeNode(SyntaxNodeAnalysisContext context) locations, properties: null)); - FadeOutCode(context, matches.Value, locations); + FadeOutCode(context, matches, locations); } private void FadeOutCode( diff --git a/src/Analyzers/Core/Analyzers/UseObjectInitializer/UseNamedMemberInitializerAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseObjectInitializer/UseNamedMemberInitializerAnalyzer.cs index 8c3ba7b311039..f011d650a7c18 100644 --- a/src/Analyzers/Core/Analyzers/UseObjectInitializer/UseNamedMemberInitializerAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseObjectInitializer/UseNamedMemberInitializerAnalyzer.cs @@ -32,14 +32,14 @@ internal class UseNamedMemberInitializerAnalyzer< private static readonly ObjectPool> s_pool = SharedPools.Default>(); - public static ImmutableArray>? Analyze( + public static ImmutableArray> Analyze( SemanticModel semanticModel, ISyntaxFacts syntaxFacts, TObjectCreationExpressionSyntax objectCreationExpression, CancellationToken cancellationToken) { var analyzer = s_pool.Allocate(); - analyzer.Initialize(semanticModel, syntaxFacts, objectCreationExpression, cancellationToken); + analyzer.Initialize(semanticModel, syntaxFacts, objectCreationExpression, analyzeForCollectionExpression: false, cancellationToken); try { return analyzer.AnalyzeWorker(); diff --git a/src/Analyzers/Core/CodeFixes/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs index 95ea7f625346c..5b9fbadb6e04a 100644 --- a/src/Analyzers/Core/CodeFixes/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Analyzers.UseCollectionInitializer; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; @@ -15,6 +16,7 @@ using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.UseCollectionInitializer @@ -27,6 +29,7 @@ internal abstract class AbstractUseCollectionInitializerCodeFixProvider< TMemberAccessExpressionSyntax, TInvocationExpressionSyntax, TExpressionStatementSyntax, + TForeachStatementSyntax, TVariableDeclaratorSyntax> : SyntaxEditorBasedCodeFixProvider where TSyntaxKind : struct @@ -36,23 +39,30 @@ internal abstract class AbstractUseCollectionInitializerCodeFixProvider< where TMemberAccessExpressionSyntax : TExpressionSyntax where TInvocationExpressionSyntax : TExpressionSyntax where TExpressionStatementSyntax : TStatementSyntax + where TForeachStatementSyntax : TStatementSyntax where TVariableDeclaratorSyntax : SyntaxNode { - public override ImmutableArray FixableDiagnosticIds + public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(IDEDiagnosticIds.UseCollectionInitializerDiagnosticId); - protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) + protected abstract TStatementSyntax GetNewStatement( + SourceText sourceText, TStatementSyntax statement, TObjectCreationExpressionSyntax objectCreation, int wrappingLength, bool useCollectionExpression, ImmutableArray> matches); + + protected sealed override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) => !diagnostic.Descriptor.ImmutableCustomTags().Contains(WellKnownDiagnosticTags.Unnecessary); - public override Task RegisterCodeFixesAsync(CodeFixContext context) + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) { RegisterCodeFix(context, AnalyzersResources.Collection_initialization_can_be_simplified, nameof(AnalyzersResources.Collection_initialization_can_be_simplified)); return Task.CompletedTask; } - protected override async Task FixAllAsync( - Document document, ImmutableArray diagnostics, - SyntaxEditor editor, CodeActionOptionsProvider fallbackOptions, CancellationToken cancellationToken) + protected sealed override async Task FixAllAsync( + Document document, + ImmutableArray diagnostics, + SyntaxEditor editor, + CodeActionOptionsProvider fallbackOptions, + CancellationToken cancellationToken) { // Fix-All for this feature is somewhat complicated. As Collection-Initializers // could be arbitrarily nested, we have to make sure that any edits we make @@ -65,53 +75,58 @@ protected override async Task FixAllAsync( var syntaxFacts = document.GetRequiredLanguageService(); var originalRoot = editor.OriginalRoot; - var originalObjectCreationNodes = new Stack(); + var originalObjectCreationNodes = new Stack<(TObjectCreationExpressionSyntax objectCreationExpression, bool useCollectionExpression)>(); foreach (var diagnostic in diagnostics) { var objectCreation = (TObjectCreationExpressionSyntax)originalRoot.FindNode( diagnostic.AdditionalLocations[0].SourceSpan, getInnermostNodeForTie: true); - originalObjectCreationNodes.Push(objectCreation); + originalObjectCreationNodes.Push((objectCreation, diagnostic.Properties?.ContainsKey(UseCollectionInitializerHelpers.UseCollectionExpressionName) is true)); } // We're going to be continually editing this tree. Track all the nodes we // care about so we can find them across each edit. - document = document.WithSyntaxRoot(originalRoot.TrackNodes(originalObjectCreationNodes)); + document = document.WithSyntaxRoot(originalRoot.TrackNodes(originalObjectCreationNodes.Select(static t => t.objectCreationExpression))); + var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var currentRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + // the option is currently not an editorconfig option, so not available in code style layer + var wrappingLength = +#if !CODE_STYLE + fallbackOptions.GetOptions(document.Project.Services)?.CollectionExpressionWrappingLength ?? +#endif + CodeActionOptions.DefaultCollectionExpressionWrappingLength; + while (originalObjectCreationNodes.Count > 0) { - var originalObjectCreation = originalObjectCreationNodes.Pop(); + var (originalObjectCreation, useCollectionExpression) = originalObjectCreationNodes.Pop(); var objectCreation = currentRoot.GetCurrentNodes(originalObjectCreation).Single(); - var matches = UseCollectionInitializerAnalyzer.Analyze( - semanticModel, syntaxFacts, objectCreation, cancellationToken); + var matches = UseCollectionInitializerAnalyzer.Analyze( + semanticModel, syntaxFacts, objectCreation, useCollectionExpression, cancellationToken); - if (matches == null || matches.Value.Length == 0) + if (matches.IsDefaultOrEmpty) continue; var statement = objectCreation.FirstAncestorOrSelf(); Contract.ThrowIfNull(statement); - var newStatement = GetNewStatement(statement, objectCreation, matches.Value) + var newStatement = GetNewStatement(sourceText, statement, objectCreation, wrappingLength, useCollectionExpression, matches) .WithAdditionalAnnotations(Formatter.Annotation); var subEditor = new SyntaxEditor(currentRoot, document.Project.Solution.Services); subEditor.ReplaceNode(statement, newStatement); foreach (var match in matches) - subEditor.RemoveNode(match, SyntaxRemoveOptions.KeepUnbalancedDirectives); + subEditor.RemoveNode(match.Statement, SyntaxRemoveOptions.KeepUnbalancedDirectives); document = document.WithSyntaxRoot(subEditor.GetChangedRoot()); + sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); currentRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); } editor.ReplaceNode(originalRoot, currentRoot); } - - protected abstract TStatementSyntax GetNewStatement( - TStatementSyntax statement, TObjectCreationExpressionSyntax objectCreation, - ImmutableArray matches); } } diff --git a/src/Analyzers/Core/CodeFixes/UseObjectInitializer/AbstractUseObjectInitializerCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UseObjectInitializer/AbstractUseObjectInitializerCodeFixProvider.cs index bdc75219a9d68..d91c13d40a8ae 100644 --- a/src/Analyzers/Core/CodeFixes/UseObjectInitializer/AbstractUseObjectInitializerCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UseObjectInitializer/AbstractUseObjectInitializerCodeFixProvider.cs @@ -86,15 +86,13 @@ protected override async Task FixAllAsync( var matches = UseNamedMemberInitializerAnalyzer.Analyze( semanticModel, syntaxFacts, objectCreation, cancellationToken); - if (matches == null || matches.Value.Length == 0) - { + if (matches.IsDefaultOrEmpty) continue; - } var statement = objectCreation.FirstAncestorOrSelf(); Contract.ThrowIfNull(statement); - var newStatement = GetNewStatement(statement, objectCreation, matches.Value) + var newStatement = GetNewStatement(statement, objectCreation, matches) .WithAdditionalAnnotations(Formatter.Annotation); var subEditor = new SyntaxEditor(currentRoot, document.Project.Solution.Services); diff --git a/src/Analyzers/VisualBasic/Analyzers/UseCollectionInitializer/VisualBasicUseCollectionInitializerDiagnosticAnalyzer.vb b/src/Analyzers/VisualBasic/Analyzers/UseCollectionInitializer/VisualBasicUseCollectionInitializerDiagnosticAnalyzer.vb index 8484123e32003..c5c1f66a95662 100644 --- a/src/Analyzers/VisualBasic/Analyzers/UseCollectionInitializer/VisualBasicUseCollectionInitializerDiagnosticAnalyzer.vb +++ b/src/Analyzers/VisualBasic/Analyzers/UseCollectionInitializer/VisualBasicUseCollectionInitializerDiagnosticAnalyzer.vb @@ -2,6 +2,7 @@ ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. +Imports System.Threading Imports Microsoft.CodeAnalysis.Diagnostics Imports Microsoft.CodeAnalysis.LanguageService Imports Microsoft.CodeAnalysis.UseCollectionInitializer @@ -19,12 +20,21 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.UseCollectionInitializer MemberAccessExpressionSyntax, InvocationExpressionSyntax, ExpressionStatementSyntax, + ForEachStatementSyntax, VariableDeclaratorSyntax) Protected Overrides Function AreCollectionInitializersSupported(compilation As Compilation) As Boolean Return True End Function + Protected Overrides Function AreCollectionExpressionsSupported(compilation As Compilation) As Boolean + Return False + End Function + + Protected Overrides Function CanUseCollectionExpression(semanticModel As SemanticModel, objectCreationExpression As ObjectCreationExpressionSyntax, cancellationToken As CancellationToken) As Boolean + Throw ExceptionUtilities.Unreachable() + End Function + Protected Overrides Function GetSyntaxFacts() As ISyntaxFacts Return VisualBasicSyntaxFacts.Instance End Function diff --git a/src/Analyzers/VisualBasic/CodeFixes/UseCollectionInitializer/VisualBasicUseCollectionInitializerCodeFixProvider.vb b/src/Analyzers/VisualBasic/CodeFixes/UseCollectionInitializer/VisualBasicUseCollectionInitializerCodeFixProvider.vb index 460742a47e92a..5f2529f168a1c 100644 --- a/src/Analyzers/VisualBasic/CodeFixes/UseCollectionInitializer/VisualBasicUseCollectionInitializerCodeFixProvider.vb +++ b/src/Analyzers/VisualBasic/CodeFixes/UseCollectionInitializer/VisualBasicUseCollectionInitializerCodeFixProvider.vb @@ -7,6 +7,7 @@ Imports System.Composition Imports System.Diagnostics.CodeAnalysis Imports Microsoft.CodeAnalysis.CodeFixes Imports Microsoft.CodeAnalysis.PooledObjects +Imports Microsoft.CodeAnalysis.Text Imports Microsoft.CodeAnalysis.UseCollectionInitializer Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Imports Microsoft.CodeAnalysis.VisualBasic.UseObjectInitializer @@ -22,6 +23,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.UseCollectionInitializer MemberAccessExpressionSyntax, InvocationExpressionSyntax, ExpressionStatementSyntax, + ForEachStatementSyntax, VariableDeclaratorSyntax) @@ -30,8 +32,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.UseCollectionInitializer End Sub Protected Overrides Function GetNewStatement( - statement As StatementSyntax, objectCreation As ObjectCreationExpressionSyntax, - matches As ImmutableArray(Of ExpressionStatementSyntax)) As StatementSyntax + sourceText As SourceText, + statement As StatementSyntax, + objectCreation As ObjectCreationExpressionSyntax, + wrappingLength As Integer, + useCollectionExpression As Boolean, + matches As ImmutableArray(Of Match(Of StatementSyntax))) As StatementSyntax + Contract.ThrowIfTrue(useCollectionExpression, "VB does not support collection expressions") Dim newStatement = statement.ReplaceNode( objectCreation, GetNewObjectCreation(objectCreation, matches)) @@ -41,7 +48,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.UseCollectionInitializer totalTrivia.Add(SyntaxFactory.ElasticMarker) For Each match In matches - For Each trivia In match.GetLeadingTrivia() + For Each trivia In match.Statement.GetLeadingTrivia() If trivia.Kind = SyntaxKind.CommentTrivia Then totalTrivia.Add(trivia) totalTrivia.Add(SyntaxFactory.ElasticMarker) @@ -54,7 +61,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.UseCollectionInitializer Private Shared Function GetNewObjectCreation( objectCreation As ObjectCreationExpressionSyntax, - matches As ImmutableArray(Of ExpressionStatementSyntax)) As ObjectCreationExpressionSyntax + matches As ImmutableArray(Of Match(Of StatementSyntax))) As ObjectCreationExpressionSyntax Return UseInitializerHelpers.GetNewObjectCreation( objectCreation, @@ -64,13 +71,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.UseCollectionInitializer Private Shared Function CreateCollectionInitializer( objectCreation As ObjectCreationExpressionSyntax, - matches As ImmutableArray(Of ExpressionStatementSyntax)) As CollectionInitializerSyntax + matches As ImmutableArray(Of Match(Of StatementSyntax))) As CollectionInitializerSyntax Dim nodesAndTokens = ArrayBuilder(Of SyntaxNodeOrToken).GetInstance() AddExistingItems(objectCreation, nodesAndTokens) For i = 0 To matches.Length - 1 - Dim expressionStatement = matches(i) + Dim expressionStatement = DirectCast(matches(i).Statement, ExpressionStatementSyntax) Dim newExpression As ExpressionSyntax Dim invocationExpression = DirectCast(expressionStatement.Expression, InvocationExpressionSyntax) diff --git a/src/Features/CSharp/Portable/BraceCompletion/BracketBraceCompletionService.cs b/src/Features/CSharp/Portable/BraceCompletion/BracketBraceCompletionService.cs index bce21752a0f9f..5533031cdf67d 100644 --- a/src/Features/CSharp/Portable/BraceCompletion/BracketBraceCompletionService.cs +++ b/src/Features/CSharp/Portable/BraceCompletion/BracketBraceCompletionService.cs @@ -69,7 +69,7 @@ public override void AddAlignTokensOperations(List list, S base.AddAlignTokensOperations(list, node, in nextOperation); var bracketPair = node.GetBracketPair(); - if (bracketPair.IsValidBracketOrBracePair() && node is ListPatternSyntax) + if (bracketPair.IsValidBracketOrBracePair() && node is ListPatternSyntax or CollectionExpressionSyntax) { // For list patterns we format brackets as though they are a block, so ensure the close bracket // is aligned with the open bracket diff --git a/src/Features/LanguageServer/Protocol/Features/Options/CodeActionOptionsStorage.cs b/src/Features/LanguageServer/Protocol/Features/Options/CodeActionOptionsStorage.cs index 06d76fcff4683..bb07b3685be4a 100644 --- a/src/Features/LanguageServer/Protocol/Features/Options/CodeActionOptionsStorage.cs +++ b/src/Features/LanguageServer/Protocol/Features/Options/CodeActionOptionsStorage.cs @@ -33,6 +33,7 @@ public static CodeActionOptions GetCodeActionOptions(this IGlobalOptionService g HideAdvancedMembers = globalOptions.GetOption(CompletionOptionsStorage.HideAdvancedMembers, languageServices.Language), WrappingColumn = globalOptions.GetOption(WrappingColumn, languageServices.Language), ConditionalExpressionWrappingLength = globalOptions.GetOption(ConditionalExpressionWrappingLength, languageServices.Language), + CollectionExpressionWrappingLength = globalOptions.GetOption(CollectionExpressionWrappingLength, languageServices.Language), }; internal static CodeActionOptionsProvider GetCodeActionOptionsProvider(this IGlobalOptionService globalOptions) @@ -43,5 +44,8 @@ internal static CodeActionOptionsProvider GetCodeActionOptionsProvider(this IGlo public static readonly PerLanguageOption2 ConditionalExpressionWrappingLength = new( "dotnet_conditional_expression_wrapping_length", CodeActionOptions.DefaultConditionalExpressionWrappingLength); + + public static readonly PerLanguageOption2 CollectionExpressionWrappingLength = new( + "dotnet_collection_expression_wrapping_length", CodeActionOptions.DefaultCollectionExpressionWrappingLength); } } diff --git a/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs b/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs index 1296ceb34c8f0..a7cc97d7e4270 100644 --- a/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs +++ b/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs @@ -421,6 +421,7 @@ public bool TryFetch(LocalUserRegistryOptionPersister persister, OptionKey2 opti {"dotnet_compute_task_list_items_for_closed_files", new RoamingProfileStorage("TextEditor.Specific.ComputeTaskListItemsForClosedFiles")}, {"dotnet_task_list_storage_descriptors", new RoamingProfileStorage("Microsoft.VisualStudio.ErrorListPkg.Shims.TaskListOptions.CommentTokens")}, {"dotnet_conditional_expression_wrapping_length", new RoamingProfileStorage("TextEditor.%LANGUAGE%.Specific.ConditionalExpressionWrappingLength")}, + {"dotnet_collection_expression_wrapping_length", new RoamingProfileStorage("TextEditor.%LANGUAGE%.Specific.CollectionExpressionWrappingLength")}, {"dotnet_report_invalid_placeholders_in_string_dot_format_calls", new RoamingProfileStorage("TextEditor.%LANGUAGE%.Specific.WarnOnInvalidStringDotFormatCalls")}, {"visual_basic_preferred_modifier_order", new RoamingProfileStorage("TextEditor.VisualBasic.Specific.PreferredModifierOrder")}, {"visual_basic_style_prefer_isnot_expression", new RoamingProfileStorage("TextEditor.VisualBasic.Specific.PreferIsNotExpression")}, diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/LanguageVersionExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/LanguageVersionExtensions.cs index 3140330f86941..a470480aa33f5 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/LanguageVersionExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/LanguageVersionExtensions.cs @@ -6,14 +6,20 @@ namespace Microsoft.CodeAnalysis.CSharp.Shared.Extensions { internal static class LanguageVersionExtensions { + public static bool IsCSharp12OrAbove(this LanguageVersion languageVersion) + => languageVersion >= LanguageVersion.Preview; + public static bool IsCSharp11OrAbove(this LanguageVersion languageVersion) => languageVersion >= LanguageVersion.CSharp11; public static bool HasConstantInterpolatedStrings(this LanguageVersion languageVersion) => languageVersion >= LanguageVersion.CSharp10; - public static bool IsCSharp12OrAbove(this LanguageVersion languageVersion) - => languageVersion >= LanguageVersion.Preview; + public static bool SupportsCollectionExpressions(this LanguageVersion languageVersion) + => languageVersion.IsCSharp12OrAbove(); + + public static bool SupportsPrimaryConstructors(this LanguageVersion languageVersion) + => languageVersion.IsCSharp12OrAbove(); /// /// Corresponds to Microsoft.CodeAnalysis.CSharp.LanguageVersionFacts.CSharpNext. diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs index 117f471630ab4..4d552a1eb4601 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/SyntaxNodeExtensions.cs @@ -808,18 +808,17 @@ public static (SyntaxToken openParen, SyntaxToken closeParen) GetParentheses(thi } public static (SyntaxToken openBracket, SyntaxToken closeBracket) GetBrackets(this SyntaxNode? node) - { - switch (node) + => node switch { - case ArrayRankSpecifierSyntax n: return (n.OpenBracketToken, n.CloseBracketToken); - case BracketedArgumentListSyntax n: return (n.OpenBracketToken, n.CloseBracketToken); - case ImplicitArrayCreationExpressionSyntax n: return (n.OpenBracketToken, n.CloseBracketToken); - case AttributeListSyntax n: return (n.OpenBracketToken, n.CloseBracketToken); - case BracketedParameterListSyntax n: return (n.OpenBracketToken, n.CloseBracketToken); - case ListPatternSyntax n: return (n.OpenBracketToken, n.CloseBracketToken); - default: return default; - } - } + ArrayRankSpecifierSyntax n => (n.OpenBracketToken, n.CloseBracketToken), + BracketedArgumentListSyntax n => (n.OpenBracketToken, n.CloseBracketToken), + ImplicitArrayCreationExpressionSyntax n => (n.OpenBracketToken, n.CloseBracketToken), + AttributeListSyntax n => (n.OpenBracketToken, n.CloseBracketToken), + BracketedParameterListSyntax n => (n.OpenBracketToken, n.CloseBracketToken), + ListPatternSyntax n => (n.OpenBracketToken, n.CloseBracketToken), + CollectionExpressionSyntax n => (n.OpenBracketToken, n.CloseBracketToken), + _ => default, + }; public static SyntaxTokenList GetModifiers(this SyntaxNode? member) => member switch diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/FormattingHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/FormattingHelpers.cs index 9beaa6d7d3e2d..b2c51ad917ca3 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/FormattingHelpers.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/FormattingHelpers.cs @@ -536,5 +536,8 @@ public static bool IsCommaInTupleExpression(this SyntaxToken currentToken) return currentToken.IsKind(SyntaxKind.CommaToken) && currentToken.Parent.IsKind(SyntaxKind.TupleExpression); } + + public static bool IsCommaInCollectionExpression(this SyntaxToken token) + => token.Kind() == SyntaxKind.CommaToken && token.Parent.IsKind(SyntaxKind.CollectionExpression); } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/ElasticTriviaFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/ElasticTriviaFormattingRule.cs index 85721afca1226..a786ab17579fc 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/ElasticTriviaFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/ElasticTriviaFormattingRule.cs @@ -33,6 +33,8 @@ public override void AddSuppressOperations(List list, SyntaxN AddPropertyDeclarationSuppressOperations(list, node); AddInitializerSuppressOperations(list, node); + + AddCollectionExpressionSuppressOperations(list, node); } private static void AddPropertyDeclarationSuppressOperations(List list, SyntaxNode node) @@ -64,6 +66,15 @@ private static void AddInitializerSuppressOperations(List lis } } + private static void AddCollectionExpressionSuppressOperations(List list, SyntaxNode node) + { + if (node is CollectionExpressionSyntax { OpenBracketToken.IsMissing: false, CloseBracketToken.IsMissing: false } collectionExpression) + { + AddSuppressWrappingIfOnSingleLineOperation(list, collectionExpression.OpenBracketToken, collectionExpression.CloseBracketToken, SuppressOption.IgnoreElasticWrapping); + return; + } + } + private static InitializerExpressionSyntax? GetInitializerNode(SyntaxNode node) => node switch { @@ -278,8 +289,9 @@ private static bool TryGetOperationBeforeDocComment(SyntaxToken currentToken, [N // then, engine will pick new line operation and ignore space operation // make attributes have a space following - if (previousToken.IsKind(SyntaxKind.CloseBracketToken) && previousToken.Parent is AttributeListSyntax - && !(currentToken.Parent is AttributeListSyntax)) + if (previousToken.IsKind(SyntaxKind.CloseBracketToken) && + previousToken.Parent is AttributeListSyntax && + currentToken.Parent is not AttributeListSyntax) { return CreateAdjustSpacesOperation(1, AdjustSpacesOption.ForceSpaces); } @@ -382,16 +394,11 @@ private static int LineBreaksAfter(SyntaxToken previousToken, SyntaxToken curren // a blank line separating them. if (currentToken.Parent is AttributeListSyntax parent) { - if (parent.Target != null) + if (parent.Target != null && + parent.Target.Identifier.Kind() is SyntaxKind.AssemblyKeyword or SyntaxKind.ModuleKeyword && + previousToken.Parent is not AttributeListSyntax) { - if (parent.Target.Identifier == SyntaxFactory.Token(SyntaxKind.AssemblyKeyword) || - parent.Target.Identifier == SyntaxFactory.Token(SyntaxKind.ModuleKeyword)) - { - if (previousToken.Parent is not AttributeListSyntax) - { - return 2; - } - } + return 2; } // Attributes on parameters should have no lines between them. diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/IndentBlockFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/IndentBlockFormattingRule.cs index 33aa1c70150c8..2814def09267a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/IndentBlockFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/IndentBlockFormattingRule.cs @@ -253,7 +253,7 @@ private static void AddBracketIndentationOperation(List li return; } - if (node.IsKind(SyntaxKind.ListPattern) && node.Parent != null) + if (node.Parent != null && node.Kind() is SyntaxKind.ListPattern or SyntaxKind.CollectionExpression) { // Brackets in list patterns are formatted like blocks, so align close bracket with open bracket AddAlignmentBlockOperationRelativeToFirstTokenOnBaseTokenLine(list, bracketPair); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/SpacingFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/SpacingFormattingRule.cs index 20a6041c73759..30c3f24f28af8 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/SpacingFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/SpacingFormattingRule.cs @@ -204,7 +204,7 @@ public override AbstractFormattingRule WithOptions(SyntaxFormattingOptions optio } // List patterns - if (currentKind == SyntaxKind.OpenBracketToken && currentToken.Parent.IsKind(SyntaxKind.ListPattern)) + if (currentKind == SyntaxKind.OpenBracketToken && currentToken.Parent.Kind() is SyntaxKind.ListPattern or SyntaxKind.CollectionExpression) { // For the space after the middle comma in ([1, 2], [1, 2]) if (previousKind == SyntaxKind.CommaToken) @@ -279,6 +279,7 @@ public override AbstractFormattingRule WithOptions(SyntaxFormattingOptions optio // For spacing delimiters - after comma if ((previousToken.IsCommaInArgumentOrParameterList() && currentKind != SyntaxKind.OmittedTypeArgumentToken) || previousToken.IsCommaInInitializerExpression() + || previousToken.IsCommaInCollectionExpression() || (previousKind == SyntaxKind.CommaToken && currentKind != SyntaxKind.OmittedArraySizeExpressionToken && HasFormattableBracketParent(previousToken))) @@ -289,6 +290,7 @@ public override AbstractFormattingRule WithOptions(SyntaxFormattingOptions optio // For spacing delimiters - before comma if ((currentToken.IsCommaInArgumentOrParameterList() && previousKind != SyntaxKind.OmittedTypeArgumentToken) || currentToken.IsCommaInInitializerExpression() + || previousToken.IsCommaInCollectionExpression() || (currentKind == SyntaxKind.CommaToken && previousKind != SyntaxKind.OmittedArraySizeExpressionToken && HasFormattableBracketParent(currentToken))) @@ -597,7 +599,7 @@ private static AdjustSpacesOperation AdjustSpacesOperationZeroOrOne(bool option, } private static bool HasFormattableBracketParent(SyntaxToken token) - => token.Parent is (kind: SyntaxKind.ArrayRankSpecifier or SyntaxKind.BracketedArgumentList or SyntaxKind.BracketedParameterList or SyntaxKind.ImplicitArrayCreationExpression or SyntaxKind.ListPattern); + => token.Parent is (kind: SyntaxKind.ArrayRankSpecifier or SyntaxKind.BracketedArgumentList or SyntaxKind.BracketedParameterList or SyntaxKind.ImplicitArrayCreationExpression or SyntaxKind.ListPattern or SyntaxKind.CollectionExpression); private static bool IsFunctionLikeKeywordExpressionKind(SyntaxKind syntaxKind) => (syntaxKind is SyntaxKind.TypeOfExpression or SyntaxKind.DefaultExpression or SyntaxKind.SizeOfExpression); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/SuppressFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/SuppressFormattingRule.cs index d52b7713ee00f..45e45068670c4 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/SuppressFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/SuppressFormattingRule.cs @@ -208,8 +208,7 @@ private static void AddSpecificNodesSuppressOperations(List l } } - if (node is AnonymousFunctionExpressionSyntax or - LocalFunctionStatementSyntax) + if (node is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax) { AddSuppressWrappingIfOnSingleLineOperation(list, node.GetFirstToken(includeZeroWidth: true), @@ -377,7 +376,7 @@ private static void AddInitializerSuppressOperations(List lis } var initializer = GetInitializerNode(node); - if (initializer is { Parent: { } }) + if (initializer?.Parent != null) { AddInitializerSuppressOperations(list, initializer.Parent, initializer.Expressions); return; @@ -388,6 +387,12 @@ private static void AddInitializerSuppressOperations(List lis AddInitializerSuppressOperations(list, anonymousCreationNode, anonymousCreationNode.Initializers); return; } + + if (node is CollectionExpressionSyntax collectionExpression) + { + AddInitializerSuppressOperations(list, collectionExpression, collectionExpression.Elements); + return; + } } private static void AddInitializerSuppressOperations(List list, SyntaxNode parent, IEnumerable items) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/TokenBasedFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/TokenBasedFormattingRule.cs index acca0a7a600f9..81534a20fc802 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/TokenBasedFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/TokenBasedFormattingRule.cs @@ -91,6 +91,7 @@ public override AbstractFormattingRule WithOptions(SyntaxFormattingOptions optio !currentToken.IsCommaInInitializerExpression() && !currentToken.IsCommaInAnyArgumentsList() && !currentToken.IsCommaInTupleExpression() && + !currentToken.IsCommaInCollectionExpression() && !currentToken.IsParenInArgumentList() && !currentToken.IsDotInMemberAccess() && !currentToken.IsCloseParenInStatement() && @@ -117,9 +118,10 @@ public override AbstractFormattingRule WithOptions(SyntaxFormattingOptions optio // statement related operations // object and anonymous initializer "," case if (previousToken.IsCommaInInitializerExpression()) - { return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines); - } + + if (previousToken.IsCommaInCollectionExpression()) + return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines); // , * in switch expression arm // ``` diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs index 06eb3fb10da5e..690b5736dcb20 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs @@ -1348,9 +1348,6 @@ public static SyntaxNode GetExpressionOfInvocationExpression(SyntaxNode node) public bool IsExpressionOfForeach([NotNullWhen(true)] SyntaxNode? node) => node?.Parent is ForEachStatementSyntax foreachStatement && foreachStatement.Expression == node; - public SyntaxNode GetExpressionOfForeachStatement(SyntaxNode node) - => ((CommonForEachStatementSyntax)node).Expression; - public SyntaxNode GetExpressionOfExpressionStatement(SyntaxNode node) => ((ExpressionStatementSyntax)node).Expression; @@ -1617,6 +1614,18 @@ public void GetPartsOfConditionalExpression(SyntaxNode node, out SyntaxNode cond whenFalse = conditionalExpression.WhenFalse; } + public void GetPartsOfForeachStatement(SyntaxNode statement, out SyntaxToken identifier, out SyntaxNode expression, out IEnumerable statements) + { + var commonForeach = (CommonForEachStatementSyntax)statement; + identifier = commonForeach is ForEachStatementSyntax { Identifier: var foreachIdentifier } + ? foreachIdentifier + : default; + expression = commonForeach.Expression; + statements = commonForeach.Statement is BlockSyntax block + ? block.Statements + : SpecializedCollections.SingletonEnumerable(commonForeach.Statement); + } + public void GetPartsOfGenericName(SyntaxNode node, out SyntaxToken identifier, out SeparatedSyntaxList typeArguments) { var genericName = (GenericNameSyntax)node; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxNodeOrTokenExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxNodeOrTokenExtensions.cs index 534255ae25d9b..7393d239d233b 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxNodeOrTokenExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxNodeOrTokenExtensions.cs @@ -33,5 +33,11 @@ public static IEnumerable DepthFirstTraversal(this SyntaxNode public static SyntaxTrivia[] GetTrivia(params SyntaxNodeOrToken[] nodesOrTokens) => nodesOrTokens.SelectMany(nodeOrToken => nodeOrToken.GetLeadingTrivia().Concat(nodeOrToken.GetTrailingTrivia())).ToArray(); + + public static SyntaxNodeOrToken WithAppendedTrailingTrivia(this SyntaxNodeOrToken nodeOrToken, params SyntaxTrivia[] trivia) + => WithAppendedTrailingTrivia(nodeOrToken, (IEnumerable)trivia); + + public static SyntaxNodeOrToken WithAppendedTrailingTrivia(this SyntaxNodeOrToken nodeOrToken, IEnumerable trivia) + => nodeOrToken.IsNode ? nodeOrToken.AsNode()!.WithAppendedTrailingTrivia(trivia) : nodeOrToken.AsToken().WithAppendedTrailingTrivia(trivia); } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs index 66ab211de8993..1dc1f8a966a50 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs @@ -198,7 +198,7 @@ internal interface ISyntaxFacts bool IsCastExpression([NotNullWhen(true)] SyntaxNode? node); bool IsExpressionOfForeach([NotNullWhen(true)] SyntaxNode? node); - SyntaxNode GetExpressionOfForeachStatement(SyntaxNode node); + void GetPartsOfForeachStatement(SyntaxNode statement, out SyntaxToken identifier, out SyntaxNode expression, out IEnumerable statements); void GetPartsOfTupleExpression(SyntaxNode node, out SyntaxToken openParen, out SeparatedSyntaxList arguments, out SyntaxToken closeParen) where TArgumentSyntax : SyntaxNode; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs index 322e028bc75c9..275f41c8ed63f 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs @@ -518,6 +518,12 @@ public static SyntaxNode GetExpressionOfParenthesizedExpression(this ISyntaxFact return expression; } + public static SyntaxNode GetExpressionOfForeachStatement(this ISyntaxFacts syntaxFacts, SyntaxNode node) + { + syntaxFacts.GetPartsOfForeachStatement(node, out _, out var expression, out _); + return expression; + } + public static SyntaxToken GetIdentifierOfGenericName(this ISyntaxFacts syntaxFacts, SyntaxNode node) { syntaxFacts.GetPartsOfGenericName(node, out var identifier, out _); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb index 59f77cf386645..8b5ab188f7d24 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb @@ -1424,10 +1424,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageService Return node IsNot Nothing AndAlso TryCast(node.Parent, ForEachStatementSyntax)?.Expression Is node End Function - Public Function GetExpressionOfForeachStatement(node As SyntaxNode) As SyntaxNode Implements ISyntaxFacts.GetExpressionOfForeachStatement - Return DirectCast(node, ForEachStatementSyntax).Expression - End Function - Public Function GetExpressionOfExpressionStatement(node As SyntaxNode) As SyntaxNode Implements ISyntaxFacts.GetExpressionOfExpressionStatement Return DirectCast(node, ExpressionStatementSyntax).Expression End Function @@ -1817,6 +1813,16 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageService whenFalse = conditionalExpression.WhenFalse End Sub + Public Sub GetPartsOfForeachStatement(statement As SyntaxNode, ByRef identifier As SyntaxToken, ByRef expression As SyntaxNode, ByRef statements As IEnumerable(Of SyntaxNode)) Implements ISyntaxFacts.GetPartsOfForeachStatement + Dim foreachStatement = DirectCast(statement, ForEachStatementSyntax) + Dim declarator = TryCast(foreachStatement.ControlVariable, VariableDeclaratorSyntax) + identifier = If(declarator Is Nothing, Nothing, declarator.Names(0).Identifier) + expression = foreachStatement.Expression + + Dim foreachBlock = TryCast(foreachStatement.Parent, ForEachBlockSyntax) + statements = If(foreachBlock Is Nothing, SpecializedCollections.EmptyEnumerable(Of SyntaxNode), foreachBlock.Statements) + End Sub + Public Sub GetPartsOfInterpolationExpression(node As SyntaxNode, ByRef stringStartToken As SyntaxToken, ByRef contents As SyntaxList(Of SyntaxNode), ByRef stringEndToken As SyntaxToken) Implements ISyntaxFacts.GetPartsOfInterpolationExpression Dim interpolatedStringExpression = DirectCast(node, InterpolatedStringExpressionSyntax) stringStartToken = interpolatedStringExpression.DollarSignDoubleQuoteToken diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeFixes/CodeActionOptions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeFixes/CodeActionOptions.cs index 0da3632208114..0b818eee20994 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeFixes/CodeActionOptions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeFixes/CodeActionOptions.cs @@ -50,6 +50,8 @@ internal sealed record class CodeActionOptions public const int DefaultConditionalExpressionWrappingLength = 120; + public const int DefaultCollectionExpressionWrappingLength = 120; + #if !CODE_STYLE [DataMember] public required CodeCleanupOptions CleanupOptions { get; init; } [DataMember] public required CodeGenerationOptions CodeGenerationOptions { get; init; } @@ -60,6 +62,7 @@ internal sealed record class CodeActionOptions [DataMember] public bool HideAdvancedMembers { get; init; } = false; [DataMember] public int WrappingColumn { get; init; } = DefaultWrappingColumn; [DataMember] public int ConditionalExpressionWrappingLength { get; init; } = DefaultConditionalExpressionWrappingLength; + [DataMember] public int CollectionExpressionWrappingLength { get; init; } = DefaultCollectionExpressionWrappingLength; public static CodeActionOptions GetDefault(LanguageServices languageServices) => new()