Skip to content

Commit

Permalink
Merge pull request #801 from polyadic/non-defaultable
Browse files Browse the repository at this point in the history
  • Loading branch information
bash authored Jan 14, 2025
2 parents ed9104e + 3ac3bd1 commit 56bc6b5
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 1 deletion.
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

0 comments on commit 56bc6b5

Please sign in to comment.