Nullability Detection
Learn how to analyze nullable reference types and nullable value types using Albatross.CodeAnalysis.
Overview
Nullability analysis is one of the most important features for modern C# development. Albatross.CodeAnalysis provides comprehensive support for detecting and working with both nullable reference types (introduced in C# 8) and nullable value types (Nullable<T>).
Getting Started
using Albatross.CodeAnalysis;
using Microsoft.CodeAnalysis;
Nullable Type Detection
Universal Nullability Check
The IsNullable() method works with both reference and value types:
// Universal nullability check
bool isNullable = typeSymbol.IsNullable(compilation);
// This works for:
// - string? (nullable reference type) → true
// - string (non-nullable reference type) → false
// - int? (nullable value type) → true
// - int (non-nullable value type) → false
Nullable Reference Types
For reference types, you can specifically check for nullable annotations:
// Check specifically for nullable reference types
bool isNullableRef = typeSymbol.IsNullableReferenceType();
// Examples:
// string? → true
// string → false
// object? → true
// object → false
Nullable Value Types
For value types, you can check for Nullable<T> wrapper:
// Check specifically for nullable value types (Nullable<T>)
bool isNullableValue = typeSymbol.IsNullableValueType(compilation);
// Examples:
// int? → true
// DateTime? → true
// int → false
// DateTime → false
Working with Nullable Value Types
Extracting Underlying Types
When working with Nullable<T>, you often need the underlying type:
// Try to get the underlying value type from Nullable<T>
if (typeSymbol.TryGetNullableValueType(compilation, out ITypeSymbol? underlyingType)) {
Console.WriteLine($"Underlying type: {underlyingType.Name}");
// For int?, this would output "Int32"
} else {
// Not a nullable value type
Console.WriteLine($"Type is: {typeSymbol.Name}");
}
Practical Example
public void AnalyzeProperty(IPropertySymbol property, Compilation compilation) {
var propertyType = property.Type;
if (propertyType.IsNullable(compilation)) {
if (propertyType.IsNullableReferenceType()) {
Console.WriteLine($"{property.Name} is a nullable reference type: {propertyType.Name}");
}
else if (propertyType.TryGetNullableValueType(compilation, out var underlying)) {
Console.WriteLine($"{property.Name} is nullable {underlying.Name}");
}
} else {
Console.WriteLine($"{property.Name} is non-nullable: {propertyType.Name}");
}
}
Comprehensive Example
Here's a complete example that demonstrates nullability analysis:
public class NullabilityAnalyzer {
public void AnalyzeClass(INamedTypeSymbol classSymbol, Compilation compilation) {
Console.WriteLine($"Analyzing class: {classSymbol.Name}");
foreach (var property in classSymbol.GetProperties()) {
AnalyzeProperty(property, compilation);
}
}
private void AnalyzeProperty(IPropertySymbol property, Compilation compilation) {
var type = property.Type;
var nullabilityInfo = AnalyzeNullability(type, compilation);
Console.WriteLine($" {property.Name}: {nullabilityInfo}");
}
private string AnalyzeNullability(ITypeSymbol type, Compilation compilation) {
if (!type.IsNullable(compilation)) {
return $"{type.Name} (non-nullable)";
}
if (type.IsNullableReferenceType()) {
return $"{type.Name} (nullable reference)";
}
if (type.TryGetNullableValueType(compilation, out var underlyingType)) {
return $"{underlyingType.Name}? (nullable value)";
}
return $"{type.Name} (unknown nullability)";
}
}
Usage in Source Generators
Nullability analysis is particularly useful in source generators:
[Generator]
public class NullabilityAwareGenerator : IIncrementalGenerator {
public void Initialize(IncrementalGeneratorInitializationContext context) {
// ... setup code ...
context.RegisterSourceOutput(classDeclarations, GenerateCode);
}
private void GenerateCode(SourceProductionContext context, ClassModel model) {
var compilation = context.Compilation;
var stringBuilder = new StringBuilder();
foreach (var property in model.Properties) {
if (property.Type.IsNullable(compilation)) {
// Generate null-check code
stringBuilder.AppendLine($"if ({property.Name} != null)");
stringBuilder.AppendLine("{");
stringBuilder.AppendLine($" // Use {property.Name}");
stringBuilder.AppendLine("}");
} else {
// Generate direct usage code
stringBuilder.AppendLine($"// {property.Name} is never null");
}
}
// Add generated source...
}
}
Test Examples
Here are some test cases that demonstrate the nullability detection:
public class TestClass {
public int Value { get; set; } // IsNullable: false
public int? NullableValue { get; set; } // IsNullable: true (nullable value)
public string Text { get; set; } = ""; // IsNullable: false
public string? NullableText { get; set; } // IsNullable: true (nullable reference)
public int[] Array { get; set; } = []; // IsNullable: false
public int[]? NullableArray { get; set; } // IsNullable: true (nullable reference)
}
Unit Test Example
[Theory]
[InlineData("Value", false)]
[InlineData("NullableValue", true)]
[InlineData("Text", false)]
[InlineData("NullableText", true)]
[InlineData("Array", false)]
[InlineData("NullableArray", true)]
public async Task VerifyIsNullable(string propertyName, bool expectedNullable) {
var compilation = await TestCode.CreateNet8CompilationAsync();
var classSymbol = compilation.GetRequiredSymbol("TestClass");
var property = classSymbol.GetMembers()
.OfType<IPropertySymbol>()
.First(x => x.Name == propertyName);
var actualNullable = property.Type.IsNullable(compilation);
actualNullable.Should().Be(expectedNullable);
}
Common Patterns
Null-Safe Code Generation
public string GeneratePropertyAccess(IPropertySymbol property, Compilation compilation) {
var type = property.Type;
var propertyName = property.Name;
if (type.IsNullable(compilation)) {
return $"{propertyName}?.ToString() ?? \"null\"";
} else {
return $"{propertyName}.ToString()";
}
}
Serialization Code Generation
public string GenerateSerializationCode(IPropertySymbol property, Compilation compilation) {
if (property.Type.IsNullable(compilation)) {
return $@"
if ({property.Name} != null) {{
writer.WriteProperty(""{property.Name}"", {property.Name});
}}";
} else {
return $@"writer.WriteProperty(""{property.Name}"", {property.Name});";
}
}
Best Practices
1. Always Use Compilation Context
Nullability detection requires the compilation context for accurate results:
// ✅ Good - uses compilation context
bool isNullable = typeSymbol.IsNullable(compilation);
// ❌ Avoid - less reliable for edge cases
bool isNullable = typeSymbol.CanBeReferencedByName && typeSymbol.IsReferenceType;
2. Handle Edge Cases
Always consider edge cases in your nullability analysis:
public bool IsEffectivelyNullable(ITypeSymbol type, Compilation compilation) {
// Handle the obvious cases
if (type.IsNullable(compilation)) {
return true;
}
// Handle special cases like generic type parameters
if (type.TypeKind == TypeKind.TypeParameter) {
var typeParam = (ITypeParameterSymbol)type;
// Check constraints to determine nullability
return !typeParam.HasNotNullConstraint;
}
return false;
}
3. Consistent Null Handling
When generating code, be consistent in how you handle nulls:
public class CodeGenHelper {
private readonly Compilation _compilation;
public string GenerateNullCheck(ITypeSymbol type, string variableName) {
if (type.IsNullable(_compilation)) {
return $"if ({variableName} != null)";
}
return string.Empty; // No null check needed
}
}
Related Topics
- Symbol Analysis Guide - General symbol analysis techniques
- Collection Types - Working with nullable collections
- Testing Your Analyzers - Unit testing nullability analysis
- API Reference - Complete API documentation