Skip to content

Commit 63a4b11

Browse files
feat: Enables Nullable Reference Types (#401)
1 parent a40ba88 commit 63a4b11

19 files changed

Lines changed: 223 additions & 59 deletions

.github/workflows/dotnetBuild.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ jobs:
4646
run: dotnet test --no-build --configuration Release --verbosity normal --logger trx --results-directory ./TestResults
4747

4848
- name: Create Failed Tests Playlist
49-
if: always()
49+
if: failure()
5050
uses: BenjaminMichaelis/trx-to-vsplaylist@v3
5151
with:
5252
trx-file-path: './TestResults/*.trx'
5353
test-outcomes: 'Failed'
54-
artifact-name: 'failed-tests-playlist'
54+
artifact-name: 'failed-tests-playlist'

Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
<PropertyGroup>
33
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
44
<LangVersion>14.0</LangVersion>
5+
<Nullable>enable</Nullable>
56
</PropertyGroup>
67
</Project>

IntelliTect.Analyzer/IntelliTect.Analyzer.CodeFixes/AsyncVoid.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,21 @@ public sealed override FixAllProvider GetFixAllProvider()
2626

2727
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
2828
{
29-
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
29+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
30+
if (root is null)
31+
{
32+
return;
33+
}
3034

3135
Diagnostic diagnostic = context.Diagnostics.First();
3236
Microsoft.CodeAnalysis.Text.TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
3337

3438
// Find the type declaration identified by the diagnostic.
3539
var declaration = root.FindToken(diagnosticSpan.Start).Parent as MethodDeclarationSyntax;
40+
if (declaration is null)
41+
{
42+
return;
43+
}
3644

3745
// Register a code action that will invoke the fix.
3846
context.RegisterCodeFix(
@@ -45,7 +53,8 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
4553

4654
private static async Task<Document> MakeReturnTask(Document document, MethodDeclarationSyntax declaration, CancellationToken cancellationToken)
4755
{
48-
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
56+
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false)
57+
?? throw new System.InvalidOperationException("Could not get syntax root");
4958
SyntaxNode newRoot = root.ReplaceNode(declaration.ReturnType, SyntaxFactory.ParseTypeName(typeof(Task).Name).WithTrailingTrivia(SyntaxFactory.Space));
5059
return document.WithSyntaxRoot(newRoot);
5160
}

IntelliTect.Analyzer/IntelliTect.Analyzer.CodeFixes/AttributesOnSeparateLines.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ public sealed override FixAllProvider GetFixAllProvider()
2929

3030
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
3131
{
32-
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
32+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
33+
if (root is null)
34+
{
35+
return;
36+
}
3337

3438
Diagnostic diagnostic = context.Diagnostics.First();
3539
Microsoft.CodeAnalysis.Text.TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
@@ -38,14 +42,17 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
3842
SyntaxToken declaration = root.FindToken(diagnosticSpan.Start);
3943

4044
// Find the enclosing AttributeList
41-
SyntaxNode attributeList = declaration.Parent;
42-
while (!attributeList.IsKind(SyntaxKind.AttributeList))
45+
SyntaxNode? attributeList = declaration.Parent;
46+
while (attributeList is not null && !attributeList.IsKind(SyntaxKind.AttributeList))
4347
{
4448
attributeList = attributeList.Parent;
4549
}
4650

4751
// Get the class, method or property adjacent to the AttributeList
48-
SyntaxNode parentDeclaration = attributeList.Parent;
52+
if (attributeList?.Parent is not SyntaxNode parentDeclaration)
53+
{
54+
return;
55+
}
4956

5057
// Register a code action that will invoke the fix.
5158
context.RegisterCodeFix(
@@ -78,7 +85,8 @@ private static async Task<Document> PutOnSeparateLine(Document document, SyntaxN
7885
.WithAdditionalAnnotations(Formatter.Annotation);
7986

8087
// Replace the old local declaration with the new local declaration.
81-
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
88+
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false)
89+
?? throw new InvalidOperationException("Could not get syntax root");
8290
SyntaxNode newRoot = oldRoot.ReplaceNode(parentDeclaration, newNode);
8391

8492
return document.WithSyntaxRoot(newRoot);

IntelliTect.Analyzer/IntelliTect.Analyzer.CodeFixes/FavorDirectoryEnumerationCalls.cs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,24 @@ public sealed override FixAllProvider GetFixAllProvider() =>
3030

3131
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
3232
{
33-
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
33+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
34+
if (root is null)
35+
{
36+
return;
37+
}
3438

3539
Diagnostic diagnostic = context.Diagnostics.First();
3640
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
3741

3842
// The diagnostic span covers the full invocation expression (Directory.GetFiles(...))
39-
InvocationExpressionSyntax invocation = root.FindToken(diagnosticSpan.Start)
40-
.Parent.AncestorsAndSelf()
43+
InvocationExpressionSyntax? invocation = root.FindToken(diagnosticSpan.Start)
44+
.Parent?.AncestorsAndSelf()
4145
.OfType<InvocationExpressionSyntax>()
42-
.First();
46+
.FirstOrDefault();
47+
if (invocation is null)
48+
{
49+
return;
50+
}
4351

4452
bool isGetFiles = diagnostic.Id == Analyzers.FavorDirectoryEnumerationCalls.DiagnosticId301;
4553
string title = isGetFiles ? TitleGetFiles : TitleGetDirectories;
@@ -61,7 +69,7 @@ private static async Task<Document> UseEnumerationMethodAsync(
6169
{
6270
var memberAccess = (MemberAccessExpressionSyntax)invocation.Expression;
6371

64-
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
72+
SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
6573

6674
// Rename: Directory.GetFiles(...) → Directory.EnumerateFiles(...)
6775
InvocationExpressionSyntax renamedInvocation = invocation.WithExpression(
@@ -76,7 +84,8 @@ private static async Task<Document> UseEnumerationMethodAsync(
7684
SyntaxFactory.IdentifierName("ToArray")))
7785
: renamedInvocation;
7886

79-
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
87+
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false)
88+
?? throw new System.InvalidOperationException("Could not get syntax root");
8089
SyntaxNode newRoot = oldRoot.ReplaceNode(invocation, replacement.WithAdditionalAnnotations(Formatter.Annotation));
8190

8291
if (replacement != renamedInvocation && newRoot is CompilationUnitSyntax compilationUnit)
@@ -89,10 +98,14 @@ private static async Task<Document> UseEnumerationMethodAsync(
8998

9099
private static bool NeedsToArrayWrapper(
91100
InvocationExpressionSyntax invocation,
92-
SemanticModel semanticModel,
101+
SemanticModel? semanticModel,
93102
CancellationToken ct)
94103
{
95-
SyntaxNode parent = invocation.Parent;
104+
if (semanticModel is null)
105+
{
106+
return false;
107+
}
108+
SyntaxNode? parent = invocation.Parent;
96109

97110
// string[] files = Directory.GetFiles(...) or field/property initializer
98111
if (parent is EqualsValueClauseSyntax equalsValue)
@@ -123,7 +136,7 @@ private static bool NeedsToArrayWrapper(
123136
// return Directory.GetFiles(...) in a method or local function returning string[]
124137
if (parent is ReturnStatementSyntax)
125138
{
126-
TypeSyntax returnType = invocation.Ancestors()
139+
TypeSyntax? returnType = invocation.Ancestors()
127140
.Select(a => a switch
128141
{
129142
MethodDeclarationSyntax m => m.ReturnType,
@@ -141,7 +154,7 @@ private static bool NeedsToArrayWrapper(
141154
// Expression-bodied members: string[] GetFiles() => Directory.GetFiles(...)
142155
if (parent is ArrowExpressionClauseSyntax arrow)
143156
{
144-
TypeSyntax returnType = arrow.Parent switch
157+
TypeSyntax? returnType = arrow.Parent switch
145158
{
146159
MethodDeclarationSyntax m => m.ReturnType,
147160
LocalFunctionStatementSyntax lf => lf.ReturnType,
@@ -160,7 +173,7 @@ private static bool NeedsToArrayWrapper(
160173
&& argumentList.Parent is InvocationExpressionSyntax outerInvocation
161174
&& semanticModel.GetSymbolInfo(outerInvocation, ct).Symbol is IMethodSymbol outerMethod)
162175
{
163-
IParameterSymbol targetParam;
176+
IParameterSymbol? targetParam;
164177

165178
// Named argument: SomeMethod(param: Directory.GetFiles(...))
166179
if (argument.NameColon != null)

IntelliTect.Analyzer/IntelliTect.Analyzer.CodeFixes/NamingFieldPascalUnderscore.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ public sealed override FixAllProvider GetFixAllProvider()
2727

2828
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
2929
{
30-
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
30+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
31+
if (root is null)
32+
{
33+
return;
34+
}
3135

3236
Diagnostic diagnostic = context.Diagnostics.First();
3337
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
@@ -50,8 +54,16 @@ private static async Task<Solution> MakePascalWithUnderscore(Document document,
5054
string nameWithoutUnderscore = nameOfField.TrimStart('_');
5155
string newName = "_" + char.ToUpper(nameWithoutUnderscore.First()) + nameWithoutUnderscore.Substring(1);
5256

53-
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
54-
ISymbol symbol = semanticModel.GetDeclaredSymbol(declaration.Parent, cancellationToken);
57+
SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
58+
if (semanticModel is null || declaration.Parent is null)
59+
{
60+
return document.Project.Solution;
61+
}
62+
ISymbol? symbol = semanticModel.GetDeclaredSymbol(declaration.Parent, cancellationToken);
63+
if (symbol is null)
64+
{
65+
return document.Project.Solution;
66+
}
5567
Solution solution = document.Project.Solution;
5668
SymbolRenameOptions options = new()
5769
{

IntelliTect.Analyzer/IntelliTect.Analyzer.CodeFixes/NamingIdentifierPascal.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ public sealed override FixAllProvider GetFixAllProvider()
2727

2828
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
2929
{
30-
SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
30+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
31+
if (root is null)
32+
{
33+
return;
34+
}
3135

3236
Diagnostic diagnostic = context.Diagnostics.First();
3337
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
@@ -49,8 +53,16 @@ private static async Task<Solution> MakePascal(Document document, SyntaxToken de
4953
string nameOfField = declaration.ValueText;
5054
string newName = char.ToUpper(nameOfField.First()) + nameOfField.Substring(1);
5155

52-
SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
53-
ISymbol symbol = semanticModel.GetDeclaredSymbol(declaration.Parent, cancellationToken);
56+
SemanticModel? semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
57+
if (semanticModel is null || declaration.Parent is null)
58+
{
59+
return document.Project.Solution;
60+
}
61+
ISymbol? symbol = semanticModel.GetDeclaredSymbol(declaration.Parent, cancellationToken);
62+
if (symbol is null)
63+
{
64+
return document.Project.Solution;
65+
}
5466
Solution solution = document.Project.Solution;
5567
SymbolRenameOptions options = new()
5668
{

IntelliTect.Analyzer/IntelliTect.Analyzer.Integration.Tests/AnalyzerTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ public static async Task ProcessProject(FileInfo projectFile)
3434
using var workspace = MSBuildWorkspace.Create();
3535
Project project = await workspace.OpenProjectAsync(projectFile.FullName).ConfigureAwait(false);
3636

37-
CompilationWithAnalyzers compilationWithAnalyzers = (await project.GetCompilationAsync().ConfigureAwait(false))
37+
Compilation compilation = await project.GetCompilationAsync().ConfigureAwait(false)
38+
?? throw new InvalidOperationException("Could not get compilation");
39+
CompilationWithAnalyzers compilationWithAnalyzers = compilation
3840
.WithAnalyzers(ImmutableArray.Create(GetAnalyzers().ToArray()));
3941

4042
ImmutableArray<Diagnostic> diags = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().ConfigureAwait(false);

IntelliTect.Analyzer/IntelliTect.Analyzer.Test/DateTimeConversionTests.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,86 @@ static void Main(string[] args)
339339
});
340340
}
341341

342+
[TestMethod]
343+
public void NullableDateTimeToDateTimeOffsetComparison_ProducesWarningMessage()
344+
{
345+
string source = @"
346+
using System;
347+
using System.Threading;
348+
349+
namespace ConsoleApp1
350+
{
351+
internal class Program
352+
{
353+
static void Main(string[] args)
354+
{
355+
DateTime? first = DateTime.Now;
356+
357+
Thread.Sleep(10);
358+
359+
DateTimeOffset second = DateTimeOffset.Now;
360+
361+
if (first < second)
362+
{
363+
Console.WriteLine(""Time has passed..."");
364+
}
365+
}
366+
}
367+
}";
368+
369+
VerifyCSharpDiagnostic(source,
370+
new DiagnosticResult
371+
{
372+
Id = "INTL0202",
373+
Severity = DiagnosticSeverity.Warning,
374+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
375+
Locations =
376+
[
377+
new DiagnosticResultLocation("Test0.cs", 17, 17)
378+
]
379+
});
380+
}
381+
382+
[TestMethod]
383+
public void NullableDateTimeToNullableDateTimeOffsetComparison_ProducesWarningMessage()
384+
{
385+
string source = @"
386+
using System;
387+
using System.Threading;
388+
389+
namespace ConsoleApp1
390+
{
391+
internal class Program
392+
{
393+
static void Main(string[] args)
394+
{
395+
DateTime? first = DateTime.Now;
396+
397+
Thread.Sleep(10);
398+
399+
DateTimeOffset? second = DateTimeOffset.Now;
400+
401+
if (first < second)
402+
{
403+
Console.WriteLine(""Time has passed..."");
404+
}
405+
}
406+
}
407+
}";
408+
409+
VerifyCSharpDiagnostic(source,
410+
new DiagnosticResult
411+
{
412+
Id = "INTL0202",
413+
Severity = DiagnosticSeverity.Warning,
414+
Message = "Using 'DateTimeOffset.implicit operator DateTimeOffset(DateTime)' or 'new DateTimeOffset(DateTime)' can result in unpredictable behavior",
415+
Locations =
416+
[
417+
new DiagnosticResultLocation("Test0.cs", 17, 17)
418+
]
419+
});
420+
}
421+
342422
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
343423
{
344424
return new Analyzers.BanImplicitDateTimeToDateTimeOffsetConversion();

IntelliTect.Analyzer/IntelliTect.Analyzer.Test/Helpers/CodeFixVerifier.Helper.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
34
using System.Threading;
@@ -26,7 +27,8 @@ private static async Task<Document> ApplyFix(Document document, CodeAction codeA
2627
{
2728
System.Collections.Immutable.ImmutableArray<CodeActionOperation> operations = await codeAction.GetOperationsAsync(CancellationToken.None);
2829
Solution solution = operations.OfType<ApplyChangesOperation>().Single().ChangedSolution;
29-
return solution.GetDocument(document.Id);
30+
return solution.GetDocument(document.Id)
31+
?? throw new InvalidOperationException("Could not get document from solution");
3032
}
3133

3234
/// <summary>
@@ -66,7 +68,8 @@ private static IEnumerable<Diagnostic> GetNewDiagnostics(IEnumerable<Diagnostic>
6668
/// <returns>The compiler diagnostics that were found in the code</returns>
6769
private static IEnumerable<Diagnostic> GetCompilerDiagnostics(Document document)
6870
{
69-
return document.GetSemanticModelAsync().Result.GetDiagnostics();
71+
return (document.GetSemanticModelAsync().Result
72+
?? throw new InvalidOperationException("Could not get semantic model")).GetDiagnostics();
7073
}
7174

7275
/// <summary>
@@ -77,7 +80,8 @@ private static IEnumerable<Diagnostic> GetCompilerDiagnostics(Document document)
7780
private static string GetStringFromDocument(Document document)
7881
{
7982
Document simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result;
80-
SyntaxNode root = simplifiedDoc.GetSyntaxRootAsync().Result;
83+
SyntaxNode root = simplifiedDoc.GetSyntaxRootAsync().Result
84+
?? throw new InvalidOperationException("Could not get syntax root");
8185
root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace);
8286
return root.GetText().ToString();
8387
}

0 commit comments

Comments
 (0)