Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions Funcky.Analyzers/Funcky.Analyzers.Test/NonDefaultableTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using Microsoft.CodeAnalysis.Testing;
using Xunit;
using VerifyCS = Funcky.Analyzers.Test.CSharpAnalyzerVerifier<Funcky.Analyzers.NonDefaultableAnalyzer>;

namespace Funcky.Analyzers.Test;

public sealed class NonDefaultableTest
{
private const string AttributeSource =
"""
namespace Funcky.CodeAnalysis
{
[System.AttributeUsage(System.AttributeTargets.Struct)]
internal sealed class NonDefaultableAttribute : System.Attribute { }
}
""";

[Fact]
public async Task DefaultInstantiationsOfRegularStructsGetNoDiagnostic()
{
const string inputCode =
"""
class Test
{
private void Usage()
{
_ = default(Foo);
}
}
struct Foo { }
""";
await VerifyCS.VerifyAnalyzerAsync(inputCode + AttributeSource);
}

[Fact]
public async Task DefaultInstantiationsOfAnnotatedStructsGetError()
{
const string inputCode =
"""
using Funcky.CodeAnalysis;
class Test
{
private void Usage()
{
_ = default(Foo);
_ = default(Funcky.Generic<int>);
}
}
[NonDefaultable]
struct Foo { }
namespace Funcky
{
[NonDefaultable]
struct Generic<T> { }
}
""";

DiagnosticResult[] expectedDiagnostics =
[
VerifyCS.Diagnostic().WithSpan(7, 13, 7, 25).WithArguments("Foo"),
VerifyCS.Diagnostic().WithSpan(8, 13, 8, 41).WithArguments("Generic<int>"),
];

await VerifyCS.VerifyAnalyzerAsync(inputCode + AttributeSource, expectedDiagnostics);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
λ1009 | Funcky | Error | NonDefaultableAnalyzer
54 changes: 54 additions & 0 deletions Funcky.Analyzers/Funcky.Analyzers/NonDefaultableAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Funcky.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class NonDefaultableAnalyzer : DiagnosticAnalyzer
{
public static readonly DiagnosticDescriptor DoNotUseDefault = new DiagnosticDescriptor(
id: $"{DiagnosticName.Prefix}{DiagnosticName.Usage}09",
title: "Do not use default to instantiate this type",
messageFormat: "Do not use default(...) to instantiate '{0}'",
category: nameof(Funcky),
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Values instantiated with default are in an invalid state; any member may throw an exception.");

private const string AttributeFullName = "Funcky.CodeAnalysis.NonDefaultableAttribute";

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DoNotUseDefault);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(OnCompilationStart);
}

private static void OnCompilationStart(CompilationStartAnalysisContext context)
{
if (context.Compilation.GetTypeByMetadataName(AttributeFullName) is { } nonDefaultableAttribute)
{
context.RegisterOperationAction(AnalyzeDefaultValueOperation(nonDefaultableAttribute), OperationKind.DefaultValue);
}
}

private static Action<OperationAnalysisContext> AnalyzeDefaultValueOperation(INamedTypeSymbol nonDefaultableAttribute)
=> context =>
{
var operation = (IDefaultValueOperation)context.Operation;
if (operation.Type is { } type && type.GetAttributes().Any(IsAttribute(nonDefaultableAttribute)))
{
context.ReportDiagnostic(Diagnostic.Create(
DoNotUseDefault,
operation.Syntax.GetLocation(),
messageArgs: type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)));
}
};

private static Func<AttributeData, bool> IsAttribute(INamedTypeSymbol attributeClass)
=> attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeClass);
}
2 changes: 2 additions & 0 deletions Funcky.Test/Monads/EitherTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using FsCheck;
using FsCheck.Xunit;
using Funcky.FsCheck;
Expand Down Expand Up @@ -46,6 +47,7 @@ public void CreateEitherRightAndMatchCorrectly()
}

[Fact]
[SuppressMessage("Funcky", "λ1009:Do not use default to instantiate this type", Justification = "Intentionally creating an invalid instance.")]
public void MatchThrowsWhenEitherIsCreatedWithDefault()
{
var value = default(Either<string, int>);
Expand Down
2 changes: 1 addition & 1 deletion Funcky.sln
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build Config", "Build Config", "{DD8F8450-BE23-4D6B-9C5C-7AED0ABB7531}"
ProjectSection(SolutionItems) = preProject
Directory.Build.props = Directory.Build.props
Directory.Packages.props = Directory.Packages.props
FrameworkFeatureConstants.props = FrameworkFeatureConstants.props
global.json = global.json
GlobalUsings.props = GlobalUsings.props
GlobalUsings.Test.props = GlobalUsings.Test.props
NuGet.config = NuGet.config
Directory.Packages.props = Directory.Packages.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Funcky.Xunit", "Funcky.Xunit\Funcky.Xunit.csproj", "{F2E98B0D-CC17-4576-89DE-065FF475BE6E}"
Expand Down
5 changes: 5 additions & 0 deletions Funcky/CodeAnalysis/NonDefaultableAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace Funcky.CodeAnalysis;

/// <summary>Structs annotated with this attribute should not be instantiated with <see langword="default"/>.</summary>
[AttributeUsage(AttributeTargets.Struct)]
internal sealed class NonDefaultableAttribute : Attribute;
1 change: 1 addition & 0 deletions Funcky/EitherOrBoth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Funcky;
/// EitherOrBoth values constructed using <c>default</c> are in an invalid state.
/// Any attempt to perform actions on such a value will throw a <see cref="NotSupportedException"/>.
/// </remarks>
[NonDefaultable]
public readonly struct EitherOrBoth<TLeft, TRight> : IEquatable<EitherOrBoth<TLeft, TRight>>
where TLeft : notnull
where TRight : notnull
Expand Down
1 change: 1 addition & 0 deletions Funcky/Monads/Either/Either.Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Funcky.Monads;
/// Either values constructed using <c>default</c> are in an invalid state.
/// Any attempt to perform actions on such a value will throw a <see cref="NotSupportedException"/>.
/// </remarks>
[NonDefaultable]
public readonly partial struct Either<TLeft, TRight> : IEquatable<Either<TLeft, TRight>>
where TLeft : notnull
where TRight : notnull
Expand Down
1 change: 1 addition & 0 deletions Funcky/Monads/Result/Result.Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace Funcky.Monads;

[NonDefaultable]
public readonly partial struct Result<TValidResult> : IEquatable<Result<TValidResult>>
where TValidResult : notnull
{
Expand Down
Loading