Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix enum contained flags check for partial matches in [RCS1258](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1258) ([PR](https://github.com/dotnet/roslynator/pull/1740) by @ovska)
- Fix analyzer [RCS1146](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1146) ([PR](https://github.com/dotnet/roslynator/pull/1747))
- Fix analyzer [RCS1194](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1194) ([PR](https://github.com/dotnet/roslynator/pull/1733))
- Fix analyzer [RCS1077](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1077) ([PR](https://github.com/dotnet/roslynator/pull/1742) by @MattFromRVA)
- [CLI] Fix `fix` command ignoring `--include` / `--exclude` file filter ([PR](https://github.com/dotnet/roslynator/pull/1758) by @hashiiiii)

## [4.15.0] - 2025-12-14
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -587,11 +587,26 @@ private static Task<Document> CallConvertAllInsteadOfSelectAsync(
in SimpleMemberInvocationExpressionInfo invocationInfo,
CancellationToken cancellationToken)
{
InvocationExpressionSyntax invocationExpression2 = SimpleMemberInvocationExpressionInfo(invocationInfo.Expression).InvocationExpression;
InvocationExpressionSyntax toListInvocation = invocationInfo.InvocationExpression;

InvocationExpressionSyntax newInvocationExpression = ChangeInvokedMethodName(invocationExpression2, "ConvertAll");
InvocationExpressionSyntax selectInvocation = SimpleMemberInvocationExpressionInfo(invocationInfo.Expression).InvocationExpression;

return document.ReplaceNodeAsync(invocationInfo.InvocationExpression, newInvocationExpression, cancellationToken);
InvocationExpressionSyntax newInvocationExpression = ChangeInvokedMethodName(selectInvocation, "ConvertAll");

IEnumerable<SyntaxTrivia> removedTrivia = toListInvocation.DescendantTrivia(
TextSpan.FromBounds(selectInvocation.Span.End, toListInvocation.Span.End));

if (removedTrivia.Any(f => !f.IsWhitespaceOrEndOfLineTrivia()))
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch strips all whitespace/EOL trivia and reattaches surviving comments inline. That's fine for block comments, but a single-line // comment has no terminating newline of its own — the newline is separate EndOfLineTrivia, which gets stripped here. So this input:

var x = items
    .Select(f => f.ToString()) // keep this
    .ToList();

would become:

var x = items
    .ConvertAll(f => f.ToString())// keep this;

…where the ; is now commented out → compile error. Could we preserve a trailing newline when the salvaged trivia ends in a single-line comment (or otherwise guard against it)? A //-comment variant of Test_CallConvertAllInsteadOfSelectAndToList_PreservesCommentInRemovedSegment would reproduce it.

newInvocationExpression = newInvocationExpression.WithTrailingTrivia(
removedTrivia.Where(f => !f.IsWhitespaceOrEndOfLineTrivia()).Concat(toListInvocation.GetTrailingTrivia()));
}
else
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: both branches converge on ... .Concat(toListInvocation.GetTrailingTrivia()) — when there are no comments the Where yields nothing, so this collapses to a single path:

IEnumerable<SyntaxTrivia> comments = removedTrivia.Where(f => !f.IsWhitespaceOrEndOfLineTrivia());
newInvocationExpression = newInvocationExpression.WithTrailingTrivia(
    comments.Concat(toListInvocation.GetTrailingTrivia()));

Behavior-equivalent and drops the branch. Also worth materializing removedTrivia with .ToList() since it's currently enumerated multiple times.

newInvocationExpression = newInvocationExpression.WithTrailingTrivia(toListInvocation.GetTrailingTrivia());
}

return document.ReplaceNodeAsync(toListInvocation, newInvocationExpression, cancellationToken);
}

private static Task<Document> CallSumInsteadOfSelectManyAndCountAsync(
Expand Down
105 changes: 105 additions & 0 deletions src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1551,6 +1551,111 @@ void M()
var x = q.OrderBy(f => f);
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)]
public async Task Test_CallConvertAllInsteadOfSelectAndToList_List_SemicolonNotMoved()
{
await VerifyDiagnosticAndFixAsync(@"
using System;
using System.Collections.Generic;
using System.Linq;

class C
{
void M(Action<Func<object>> action, List<object> items)
{
action(() =>
{
List<string> x = items
.[|Select(f => f.ToString())
.ToList()|];

return x;
});
}
}
", @"
using System;
using System.Collections.Generic;
using System.Linq;

class C
{
void M(Action<Func<object>> action, List<object> items)
{
action(() =>
{
List<string> x = items
.ConvertAll(f => f.ToString());

return x;
});
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)]
public async Task Test_CallConvertAllInsteadOfSelectAndToList_PreservesCommentInRemovedSegment()
{
await VerifyDiagnosticAndFixAsync(@"
using System.Collections.Generic;
using System.Linq;

class C
{
void M(List<object> items)
{
var x = items
.[|Select(f => f.ToString())
/* comment in removed segment */.ToList()|];
}
}
", @"
using System.Collections.Generic;
using System.Linq;

class C
{
void M(List<object> items)
{
var x = items
.ConvertAll(f => f.ToString())/* comment in removed segment */;
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)]
public async Task Test_CallConvertAllInsteadOfSelectAndToList_PreservesTrailingCommentAfterStatement()
{
await VerifyDiagnosticAndFixAsync(@"
using System.Collections.Generic;
using System.Linq;

class C
{
void M(List<object> items)
{
var x = items
.[|Select(f => f.ToString())
.ToList()|]; // trailing comment
}
}
", @"
using System.Collections.Generic;
using System.Linq;

class C
{
void M(List<object> items)
{
var x = items
.ConvertAll(f => f.ToString()); // trailing comment
}
}
");
}
}
Loading