diff --git a/ChangeLog.md b/ChangeLog.md index 67050b4162..072919277b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix analyzer [RCS1194](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1194) ([PR](https://github.com/dotnet/roslynator/pull/1733)) - Fix analyzer [RCS1060](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1060) to ignore classes marked with `file` modifier ([PR](https://github.com/dotnet/roslynator/pull/1777) by @cbersch) - Fix analyzer [RCS1231](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1231) ([PR](https://github.com/dotnet/roslynator/pull/1774) by @cbersch) +- Fix analyzer [RCS1246](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1246) for conditional access expressions ([PR](https://github.com/dotnet/roslynator/pull/1772 by @krajek)) - [CLI] Fix `fix` command ignoring `--include` / `--exclude` file filter ([PR](https://github.com/dotnet/roslynator/pull/1758) by @hashiiiii) - [CLI] Fix loading of projects and solutions on .NET 10 SDK ([PR](https://github.com/dotnet/roslynator/pull/1783)) diff --git a/src/Common/CSharp/Analysis/UseElementAccessAnalysis.cs b/src/Common/CSharp/Analysis/UseElementAccessAnalysis.cs index 1d96cdceb0..fa847c76f5 100644 --- a/src/Common/CSharp/Analysis/UseElementAccessAnalysis.cs +++ b/src/Common/CSharp/Analysis/UseElementAccessAnalysis.cs @@ -35,6 +35,15 @@ public static bool IsFixableElementAt( if (!HasAccessibleIndexer(typeSymbol, reducedExtensionMethodInfo.ReducedSymbolOrSymbol.ReturnType, semanticModel, invocationExpression.SpanStart)) return false; + // GetSpeculativeSymbolInfo cannot bind a MemberBindingExpression (conditional access, + // e.g. 'x?.Values.ElementAt(0)') as a standalone expression, so the speculative element + // access below would throw. That binding's only purpose is infinite-recursion detection, + // which we intentionally skip here. The recursion scenario is still technically reachable + // via conditional access (a member returning the enclosing indexer's type), but it's rare + // enough that we accept not guarding it in order to fix the crash. + if (invocationInfo.Expression.IsKind(SyntaxKind.MemberBindingExpression)) + return true; + ElementAccessExpressionSyntax elementAccess = SyntaxFactory.ElementAccessExpression( invocationInfo.Expression, CSharpFactory.BracketedArgumentList(invocationInfo.Arguments[0])); diff --git a/src/Tests/Analyzers.Tests/RCS1246UseElementAccessTests.cs b/src/Tests/Analyzers.Tests/RCS1246UseElementAccessTests.cs index fd4fb37131..7e01cb4377 100644 --- a/src/Tests/Analyzers.Tests/RCS1246UseElementAccessTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1246UseElementAccessTests.cs @@ -334,6 +334,108 @@ void M() KeyValuePair elementAt = dic.ElementAt(1); } } +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseElementAccess)] + public async Task Test_UseElementAccessInsteadOfElementAt_ConditionalAccess() + { + await VerifyDiagnosticAndFixAsync(@" +using System.Collections.Generic; +using System.Linq; + +class C +{ + void M() + { + var x = new C(); + var y = x?.Values.[|ElementAt(0)|]; + } + + public IReadOnlyList Values => new List(); +} +", @" +using System.Collections.Generic; +using System.Linq; + +class C +{ + void M() + { + var x = new C(); + var y = x?.Values[0]; + } + + public IReadOnlyList Values => new List(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseElementAccess)] + public async Task Test_UseElementAccessInsteadOfFirst_ConditionalAccess() + { + await VerifyDiagnosticAndFixAsync(@" +using System.Collections.Generic; +using System.Linq; + +class C +{ + void M() + { + var x = new C(); + var y = x?.Values.[|First()|]; + } + + public IReadOnlyList Values => new List(); +} +", @" +using System.Collections.Generic; +using System.Linq; + +class C +{ + void M() + { + var x = new C(); + var y = x?.Values[0]; + } + + public IReadOnlyList Values => new List(); +} +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UseElementAccess)] + public async Task Test_UseElementAccessInsteadOfLast_ConditionalAccess() + { + await VerifyDiagnosticAndFixAsync(@" +using System.Collections.Generic; +using System.Linq; + +class C +{ + void M() + { + var x = new C(); + var y = x?.Values.[|Last()|]; + } + + public IReadOnlyList Values => new List(); +} +", @" +using System.Collections.Generic; +using System.Linq; + +class C +{ + void M() + { + var x = new C(); + var y = x?.Values[^1]; + } + + public IReadOnlyList Values => new List(); +} "); } }