Skip to content

Commit 6b11790

Browse files
authored
Merge pull request #1187 from progressonderwijs/robin/add-analyzer
add IfNotFalseAnalyzer
2 parents 12d8af5 + 875bf8c commit 6b11790

File tree

4 files changed

+343
-2
lines changed

4 files changed

+343
-2
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace ProgressOnderwijsUtils.Analyzers;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public sealed class IfNotFalseAnalyzer : DiagnosticAnalyzer
11+
{
12+
public static readonly DiagnosticDescriptor Rule = new(
13+
#pragma warning disable RS2008 // Enable analyzer release tracking - why would we care?
14+
"POU1004",
15+
#pragma warning restore RS2008 // Enable analyzer release tracking
16+
"Redundant if (!false) statement",
17+
"The if statement condition is always true (if (!false))",
18+
"CodeQuality",
19+
DiagnosticSeverity.Warning,
20+
true
21+
);
22+
23+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
24+
=> [Rule,];
25+
26+
public override void Initialize(AnalysisContext context)
27+
{
28+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
29+
context.EnableConcurrentExecution();
30+
context.RegisterSyntaxNodeAction(AnalyzeIfStatement, SyntaxKind.IfStatement);
31+
}
32+
33+
private static void AnalyzeIfStatement(SyntaxNodeAnalysisContext context)
34+
{
35+
var ifStatement = (IfStatementSyntax)context.Node;
36+
if (ifStatement.Condition is PrefixUnaryExpressionSyntax prefix &&
37+
prefix.IsKind(SyntaxKind.LogicalNotExpression) &&
38+
prefix.Operand is LiteralExpressionSyntax literal &&
39+
literal.IsKind(SyntaxKind.FalseLiteralExpression)) {
40+
context.ReportDiagnostic(Diagnostic.Create(Rule, ifStatement.GetLocation()));
41+
}
42+
}
43+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CodeActions;
7+
using Microsoft.CodeAnalysis.CodeFixes;
8+
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
using Microsoft.CodeAnalysis.Formatting;
10+
11+
namespace ProgressOnderwijsUtils.Analyzers;
12+
13+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(IfNotFalseCodeFix))]
14+
[Shared]
15+
public sealed class IfNotFalseCodeFix : CodeFixProvider
16+
{
17+
public override ImmutableArray<string> FixableDiagnosticIds
18+
=> [IfNotFalseAnalyzer.Rule.Id,];
19+
20+
public override FixAllProvider GetFixAllProvider()
21+
=> WellKnownFixAllProviders.BatchFixer;
22+
23+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
24+
{
25+
const string title = "Remove redundant if statement";
26+
var diagnostic = context.Diagnostics.First();
27+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
28+
if (root is null) {
29+
return;
30+
}
31+
32+
var ifStatement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf<IfStatementSyntax>();
33+
if (ifStatement is null) {
34+
return;
35+
}
36+
37+
var newRoot = ifStatement.Statement is BlockSyntax { Statements.Count: > 0, } block
38+
? ReplaceWithBlockStatements(root, ifStatement, block)
39+
: root.RemoveNode(ifStatement, SyntaxRemoveOptions.KeepNoTrivia);
40+
41+
if (newRoot is null) {
42+
return;
43+
}
44+
45+
context.RegisterCodeFix(
46+
CodeAction.Create(
47+
title,
48+
_ => Task.FromResult(context.Document.WithSyntaxRoot(newRoot)),
49+
title
50+
),
51+
diagnostic
52+
);
53+
}
54+
55+
static SyntaxNode ReplaceWithBlockStatements(SyntaxNode root, IfStatementSyntax ifStatement, BlockSyntax block)
56+
{
57+
var formattedStatements = block.Statements
58+
.Select(s => s.WithAdditionalAnnotations(Formatter.Annotation))
59+
.ToArray();
60+
61+
return root.ReplaceNode(ifStatement, formattedStatements);
62+
}
63+
}

src/ProgressOnderwijsUtils.Analyzers/ProgressOnderwijsUtils.Analyzers.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
<PropertyGroup>
44
<Description>Bump dependencies</Description>
55
<PackageTags>ProgressOnderwijs</PackageTags>
6-
<PackageReleaseNotes>Added (minimal) readme file</PackageReleaseNotes>
7-
<Version>2.1.5</Version>
6+
<PackageReleaseNotes>Added IfNotFalseAnalyzer</PackageReleaseNotes>
7+
<Version>2.1.6</Version>
88
<TargetFramework>netstandard2.0</TargetFramework>
99
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
1010
<PackageReadmeFile>readme.md</PackageReadmeFile>
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
using System.Linq;
2+
using System.Threading.Tasks;
3+
using ExpressionToCodeLib;
4+
using Microsoft.CodeAnalysis;
5+
using Xunit;
6+
7+
namespace ProgressOnderwijsUtils.Analyzers.Tests;
8+
9+
public sealed class IfNotFalseAnalyzerTests
10+
{
11+
[Fact]
12+
public void FlagsIfNotFalse()
13+
{
14+
var source = """
15+
class C
16+
{
17+
void M()
18+
{
19+
if (!false) { }
20+
}
21+
}
22+
""";
23+
var diagnostics = DiagnosticHelper.GetDiagnostics(new IfNotFalseAnalyzer(), source);
24+
PAssert.That(() => diagnostics.Single().Id == IfNotFalseAnalyzer.Rule.Id);
25+
PAssert.That(() => diagnostics.Single().Location.GetLineSpan().StartLinePosition.Line == 4);
26+
}
27+
28+
[Fact]
29+
public void DoesNotFlagIfTrue()
30+
{
31+
var source = """
32+
class C
33+
{
34+
void M()
35+
{
36+
if (true) { }
37+
}
38+
}
39+
""";
40+
var diagnostics = DiagnosticHelper.GetDiagnostics(new IfNotFalseAnalyzer(), source);
41+
PAssert.That(() => diagnostics.None());
42+
}
43+
44+
[Fact]
45+
public async Task CodeFix_RemovesIfNotFalse()
46+
{
47+
var testCases = new[] {
48+
new {
49+
Source = """
50+
class C
51+
{
52+
void M()
53+
{
54+
if (!false) { System.Console.WriteLine("Hello"); }
55+
}
56+
}
57+
""",
58+
Expected = """
59+
class C
60+
{
61+
void M()
62+
{
63+
System.Console.WriteLine("Hello");
64+
}
65+
}
66+
""",
67+
},
68+
new {
69+
Source = """
70+
class C
71+
{
72+
void M()
73+
{
74+
if (!false) { System.Console.WriteLine("A"); System.Console.WriteLine("B"); }
75+
}
76+
}
77+
""",
78+
Expected = """
79+
class C
80+
{
81+
void M()
82+
{
83+
System.Console.WriteLine("A"); System.Console.WriteLine("B");
84+
}
85+
}
86+
""",
87+
},
88+
new {
89+
Source = """
90+
class C
91+
{
92+
void M()
93+
{
94+
if (!false)
95+
{
96+
System.Console.WriteLine("Hello");
97+
}
98+
}
99+
}
100+
""",
101+
Expected = """
102+
class C
103+
{
104+
void M()
105+
{
106+
System.Console.WriteLine("Hello");
107+
}
108+
}
109+
""",
110+
},
111+
new {
112+
Source = """
113+
class C
114+
{
115+
void M()
116+
{
117+
if (!false) { }
118+
}
119+
}
120+
""",
121+
Expected = """
122+
class C
123+
{
124+
void M()
125+
{
126+
}
127+
}
128+
""",
129+
},
130+
new {
131+
Source = """
132+
class C
133+
{
134+
void M()
135+
{
136+
if (!false)
137+
{
138+
System.Console.WriteLine("A");
139+
System.Console.WriteLine("B");
140+
}
141+
}
142+
}
143+
""",
144+
Expected = """
145+
class C
146+
{
147+
void M()
148+
{
149+
System.Console.WriteLine("A");
150+
System.Console.WriteLine("B");
151+
}
152+
}
153+
""",
154+
},
155+
new {
156+
Source = """
157+
class C
158+
{
159+
void M()
160+
{
161+
if (!false) { System.Console.WriteLine("A"); } else { System.Console.WriteLine("B"); }
162+
}
163+
}
164+
""",
165+
Expected = """
166+
class C
167+
{
168+
void M()
169+
{
170+
System.Console.WriteLine("A");
171+
}
172+
}
173+
""",
174+
},
175+
new {
176+
Source = """
177+
class C
178+
{
179+
void M()
180+
{
181+
if (!false) { System.Console.WriteLine("A"); } else { System.Console.WriteLine("B"); }
182+
}
183+
}
184+
""",
185+
Expected = """
186+
class C
187+
{
188+
void M()
189+
{
190+
System.Console.WriteLine("A");
191+
}
192+
}
193+
""",
194+
},
195+
new {
196+
Source = """
197+
class C
198+
{
199+
void M()
200+
{
201+
if (!false)
202+
{
203+
System.Console.WriteLine("A");
204+
}
205+
else
206+
{
207+
System.Console.WriteLine("B");
208+
}
209+
}
210+
}
211+
""",
212+
Expected = """
213+
class C
214+
{
215+
void M()
216+
{
217+
System.Console.WriteLine("A");
218+
}
219+
}
220+
""",
221+
},
222+
};
223+
224+
foreach (var test in testCases) {
225+
var workspace = DiagnosticHelper.CreateProjectWithTestFile(test.Source);
226+
var diagnostic = DiagnosticHelper.GetDiagnostics(new IfNotFalseAnalyzer(), workspace).Single(d => d.Id == IfNotFalseAnalyzer.Rule.Id); // otherwise we could get diagnostic like CS0162 (unreachable code)
227+
var fixesMade = await DiagnosticHelper.ApplyAllCodeFixes(workspace, diagnostic, new IfNotFalseCodeFix());
228+
PAssert.That(() => fixesMade == 1);
229+
230+
var result = await workspace.CurrentSolution.Projects.Single().Documents.Single().GetTextAsync();
231+
var resultText = result.ToString().Replace("\r\n", "\n");
232+
Assert.Equal(test.Expected, resultText);
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)