Skip to content

Commit

Permalink
Stop RCS1031, RCS1208 and RCS1211 from being applied if the local var…
Browse files Browse the repository at this point in the history
…iables overlap. (dotnet#1039)

Co-authored-by: Josef Pihrt <josef@pihrt.net>
  • Loading branch information
2 people authored and JochemHarmes committed Oct 30, 2023
1 parent d5a2165 commit 241a09d
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 8 deletions.
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix ([RCS1206](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1206.md)) ([#1049](https://github.com/JosefPihrt/Roslynator/pull/1049)).
- Prevent possible recursion in ([RCS1235](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1235.md)) ([#1054](https://github.com/JosefPihrt/Roslynator/pull/1054)).
- Fix ([RCS1223](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1223.md)) ([#1051](https://github.com/JosefPihrt/Roslynator/pull/1051)).
- Do not remove braces in the cases where there are overlapping local variables. ([RCS1031](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1013.md), [RCS1211](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1211.md), [RCS1208](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1208.md)) ([#1039](https://github.com/JosefPihrt/Roslynator/pull/1039)).

## [4.2.0] - 2022-11-27

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -90,6 +91,13 @@ public static void AnalyzerSwitchSection(SyntaxNodeAnalysisContext context)
if (!AnalyzeTrivia(closeBrace.TrailingTrivia))
return;

// If any of the other case blocks contain a definition for the same local variables then removing the braces would introduce a new error.
if (switchSection.Parent is SwitchStatementSyntax switchStatement
&& LocallyDeclaredVariablesOverlapWithAnyOtherSwitchSections(switchStatement, block, context.SemanticModel))
{
return;
}

DiagnosticHelpers.ReportDiagnostic(context, DiagnosticRules.RemoveUnnecessaryBracesInSwitchSection, openBrace);
DiagnosticHelpers.ReportDiagnostic(context, DiagnosticRules.RemoveUnnecessaryBracesInSwitchSectionFadeOut, closeBrace);

Expand All @@ -98,4 +106,36 @@ static bool AnalyzeTrivia(SyntaxTriviaList trivia)
return trivia.All(f => f.IsKind(SyntaxKind.WhitespaceTrivia, SyntaxKind.EndOfLineTrivia, SyntaxKind.SingleLineCommentTrivia));
}
}

private static bool LocallyDeclaredVariablesOverlapWithAnyOtherSwitchSections(SwitchStatementSyntax switchStatement, BlockSyntax switchBlock, SemanticModel semanticModel)
{
var sectionVariablesDeclared = semanticModel.AnalyzeDataFlow(switchBlock)!
.VariablesDeclared;

if (sectionVariablesDeclared.IsEmpty)
return false;

var sectionDeclaredVariablesNames = sectionVariablesDeclared
.Select(s => s.Name)
.ToImmutableHashSet();

foreach (var otherSection in switchStatement.Sections)
{
// If the other section is not a block then we do not need to check as if there were overlapping variables then there would already be a error.
if (otherSection.Statements.SingleOrDefault(shouldThrow: false) is not BlockSyntax otherBlock)
continue;

if (otherBlock.Span == switchBlock.Span)
continue;

foreach (var v in semanticModel.AnalyzeDataFlow(otherBlock)!.VariablesDeclared)
{
if (sectionDeclaredVariablesNames.Contains(v.Name))
return true;
}
}
return false;
}


}
48 changes: 41 additions & 7 deletions src/Analyzers/CSharp/Analysis/RemoveUnnecessaryElseAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -38,13 +40,13 @@ public static void Analyze(SyntaxNodeAnalysisContext context)
if (elseClause.ContainsDiagnostics)
return;

if (!IsFixable(elseClause))
if (!IsFixable(elseClause, context.SemanticModel))
return;

DiagnosticHelpers.ReportDiagnostic(context, DiagnosticRules.RemoveUnnecessaryElse, elseClause.ElseKeyword);
}

public static bool IsFixable(ElseClauseSyntax elseClause)
private static bool IsFixable(ElseClauseSyntax elseClause, SemanticModel semanticModel)
{
if (elseClause.Statement?.IsKind(SyntaxKind.IfStatement) != false)
return false;
Expand All @@ -55,12 +57,44 @@ public static bool IsFixable(ElseClauseSyntax elseClause)
if (!ifStatement.IsTopmostIf())
return false;

StatementSyntax statement = ifStatement.Statement;
StatementSyntax ifStatementStatement = ifStatement.Statement;

if (statement is BlockSyntax block)
statement = block.Statements.LastOrDefault();
if (ifStatementStatement is not BlockSyntax ifBlock)
return CSharpFacts.IsJumpStatement(ifStatementStatement.Kind());

if (elseClause.Statement is BlockSyntax elseBlock && LocalDeclaredVariablesOverlap(elseBlock, ifBlock, semanticModel))
return false;

var lastStatementInIf = ifBlock.Statements.LastOrDefault();

return lastStatementInIf is not null
&& CSharpFacts.IsJumpStatement(lastStatementInIf.Kind());
}

private static bool LocalDeclaredVariablesOverlap(BlockSyntax elseBlock, BlockSyntax ifBlock, SemanticModel semanticModel)
{
var elseVariablesDeclared = semanticModel.AnalyzeDataFlow(elseBlock)!
.VariablesDeclared;

if (elseVariablesDeclared.IsEmpty)
return false;

var ifVariablesDeclared = semanticModel.AnalyzeDataFlow(ifBlock)!
.VariablesDeclared;

if (ifVariablesDeclared.IsEmpty)
return false;

var elseVariableNames = elseVariablesDeclared
.Select(s => s.Name)
.ToImmutableHashSet();

foreach (var v in ifVariablesDeclared)
{
if (elseVariableNames.Contains(v.Name))
return true;
}

return statement is not null
&& CSharpFacts.IsJumpStatement(statement.Kind());
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
Expand Down Expand Up @@ -71,6 +72,9 @@ private static ReduceIfNestingAnalysisResult AnalyzeCore(
return Fail(switchSection);
}

if (LocallyDeclaredVariablesOverlapWithOuterScope(ifStatement, switchSection, semanticModel))
return Fail(switchSection);

return Success(jumpKind, switchSection);
}

Expand Down Expand Up @@ -104,6 +108,9 @@ private static ReduceIfNestingAnalysisResult AnalyzeCore(
return Fail(parent);
}

if (LocallyDeclaredVariablesOverlapWithOuterScope(ifStatement, parent, semanticModel))
return Fail(parent);

return Success(jumpKind, parent);
}

Expand All @@ -126,6 +133,9 @@ private static ReduceIfNestingAnalysisResult AnalyzeCore(
{
return Fail(parent);
}

if (LocallyDeclaredVariablesOverlapWithOuterScope(ifStatement, parent, semanticModel))
return Fail(parent);

return Success(jumpKind, parent);
}
Expand All @@ -135,12 +145,18 @@ private static ReduceIfNestingAnalysisResult AnalyzeCore(
{
if (jumpKind == SyntaxKind.None)
return Fail(parent);


if (LocallyDeclaredVariablesOverlapWithOuterScope(ifStatement, parent, semanticModel))
return Fail(parent);

return Success(jumpKind, parent);
}
case SyntaxKind.MethodDeclaration:
{
var methodDeclaration = (MethodDeclarationSyntax)parent;

if (LocallyDeclaredVariablesOverlapWithOuterScope(ifStatement, methodDeclaration.Body, semanticModel))
return Fail(parent);

if (jumpKind != SyntaxKind.None)
return Success(jumpKind, parent);
Expand Down Expand Up @@ -172,6 +188,9 @@ private static ReduceIfNestingAnalysisResult AnalyzeCore(
case SyntaxKind.LocalFunctionStatement:
{
var localFunction = (LocalFunctionStatementSyntax)parent;

if (LocallyDeclaredVariablesOverlapWithOuterScope(ifStatement, localFunction.Body, semanticModel))
return Fail(parent);

if (jumpKind != SyntaxKind.None)
return Success(jumpKind, parent);
Expand Down Expand Up @@ -204,6 +223,9 @@ private static ReduceIfNestingAnalysisResult AnalyzeCore(
{
var anonymousFunction = (AnonymousFunctionExpressionSyntax)parent;

if (LocallyDeclaredVariablesOverlapWithOuterScope(ifStatement, anonymousFunction.Block, semanticModel))
return Fail(parent);

if (jumpKind != SyntaxKind.None)
return Success(jumpKind, parent);

Expand Down Expand Up @@ -260,6 +282,27 @@ private static ReduceIfNestingAnalysisResult AnalyzeCore(
return Fail(parent);
}

private static bool LocallyDeclaredVariablesOverlapWithOuterScope(
IfStatementSyntax ifStatement,
SyntaxNode parent,
SemanticModel semanticModel
)
{
var ifVariablesDeclared = semanticModel.AnalyzeDataFlow(ifStatement)!
.VariablesDeclared;

if (ifVariablesDeclared.IsEmpty)
return false;

var parentStatementDeclared = semanticModel.AnalyzeDataFlow(parent)!
.VariablesDeclared;

// The parent's declared variables will include those from the if and so we have to check for any symbols occurring twice.
return ifVariablesDeclared.Any(variable =>
parentStatementDeclared.Count(s => s.Name == variable.Name) > 1
);
}

private static bool IsNestedFix(SyntaxNode node, SemanticModel semanticModel, ReduceIfNestingOptions options, CancellationToken cancellationToken)
{
options |= ReduceIfNestingOptions.AllowNestedFix;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,57 @@ void M()
}
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBracesInSwitchSection)]
public async Task Test_WithLocalVariablesThatDoNotOverlap()
{
await VerifyDiagnosticAndFixAsync(@"
using System;
class C
{
void M()
{
string s = null;
switch (s)
{
case """":
[|{|]
var x = 1;
break;
}
default:
[|{|]
var y = 1;
break;
}
}
}
}
",@"
using System;
class C
{
void M()
{
string s = null;
switch (s)
{
case """":
var x = 1;
break;
default:
var y = 1;
break;
}
}
}
");
}

Expand Down Expand Up @@ -184,6 +235,36 @@ void M()
}
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.RemoveUnnecessaryBracesInSwitchSection)]
public async Task TestNoDiagnostic_WhenOverlappingLocalVariableDeclaration()
{
await VerifyNoDiagnosticAsync(@"
using System;
class C
{
void M()
{
string s = null;
switch (s)
{
case """":
{
var x = 1;
break;
}
default:
{
var x = 1;
break;
}
}
}
}
");
}
}
29 changes: 29 additions & 0 deletions src/Tests/Analyzers.Tests/RCS1208ReduceIfNestingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,35 @@ void M2()
{
}
}
");
}

[Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.ReduceIfNesting)]
public async Task TestNoDiagnostic_OverlappingLocalVariables()
{
await VerifyNoDiagnosticAsync(@"
class C
{
void M(bool p, bool q)
{
if (p)
{
var x = 1;
M2();
}
if(q)
{
var x = 2;
M2();
}
}
void M2()
{
}
}
");
}
}
Loading

0 comments on commit 241a09d

Please sign in to comment.