Skip to content

Commit 56bc6b5

Browse files
authored
Merge pull request #801 from polyadic/non-defaultable
2 parents ed9104e + 3ac3bd1 commit 56bc6b5

File tree

9 files changed

+136
-1
lines changed

9 files changed

+136
-1
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using Microsoft.CodeAnalysis.Testing;
2+
using Xunit;
3+
using VerifyCS = Funcky.Analyzers.Test.CSharpAnalyzerVerifier<Funcky.Analyzers.NonDefaultableAnalyzer>;
4+
5+
namespace Funcky.Analyzers.Test;
6+
7+
public sealed class NonDefaultableTest
8+
{
9+
private const string AttributeSource =
10+
"""
11+
namespace Funcky.CodeAnalysis
12+
{
13+
[System.AttributeUsage(System.AttributeTargets.Struct)]
14+
internal sealed class NonDefaultableAttribute : System.Attribute { }
15+
}
16+
""";
17+
18+
[Fact]
19+
public async Task DefaultInstantiationsOfRegularStructsGetNoDiagnostic()
20+
{
21+
const string inputCode =
22+
"""
23+
class Test
24+
{
25+
private void Usage()
26+
{
27+
_ = default(Foo);
28+
}
29+
}
30+
31+
struct Foo { }
32+
""";
33+
await VerifyCS.VerifyAnalyzerAsync(inputCode + AttributeSource);
34+
}
35+
36+
[Fact]
37+
public async Task DefaultInstantiationsOfAnnotatedStructsGetError()
38+
{
39+
const string inputCode =
40+
"""
41+
using Funcky.CodeAnalysis;
42+
43+
class Test
44+
{
45+
private void Usage()
46+
{
47+
_ = default(Foo);
48+
_ = default(Funcky.Generic<int>);
49+
}
50+
}
51+
52+
[NonDefaultable]
53+
struct Foo { }
54+
55+
namespace Funcky
56+
{
57+
[NonDefaultable]
58+
struct Generic<T> { }
59+
}
60+
""";
61+
62+
DiagnosticResult[] expectedDiagnostics =
63+
[
64+
VerifyCS.Diagnostic().WithSpan(7, 13, 7, 25).WithArguments("Foo"),
65+
VerifyCS.Diagnostic().WithSpan(8, 13, 8, 41).WithArguments("Generic<int>"),
66+
];
67+
68+
await VerifyCS.VerifyAnalyzerAsync(inputCode + AttributeSource, expectedDiagnostics);
69+
}
70+
}

Funcky.Analyzers/Funcky.Analyzers/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
### New Rules
55
Rule ID | Category | Severity | Notes
66
--------|----------|----------|-------
7+
λ1009 | Funcky | Error | NonDefaultableAnalyzer
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.Diagnostics;
4+
using Microsoft.CodeAnalysis.Operations;
5+
6+
namespace Funcky.Analyzers;
7+
8+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
9+
public sealed class NonDefaultableAnalyzer : DiagnosticAnalyzer
10+
{
11+
public static readonly DiagnosticDescriptor DoNotUseDefault = new DiagnosticDescriptor(
12+
id: $"{DiagnosticName.Prefix}{DiagnosticName.Usage}09",
13+
title: "Do not use default to instantiate this type",
14+
messageFormat: "Do not use default(...) to instantiate '{0}'",
15+
category: nameof(Funcky),
16+
DiagnosticSeverity.Error,
17+
isEnabledByDefault: true,
18+
description: "Values instantiated with default are in an invalid state; any member may throw an exception.");
19+
20+
private const string AttributeFullName = "Funcky.CodeAnalysis.NonDefaultableAttribute";
21+
22+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DoNotUseDefault);
23+
24+
public override void Initialize(AnalysisContext context)
25+
{
26+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
27+
context.EnableConcurrentExecution();
28+
context.RegisterCompilationStartAction(OnCompilationStart);
29+
}
30+
31+
private static void OnCompilationStart(CompilationStartAnalysisContext context)
32+
{
33+
if (context.Compilation.GetTypeByMetadataName(AttributeFullName) is { } nonDefaultableAttribute)
34+
{
35+
context.RegisterOperationAction(AnalyzeDefaultValueOperation(nonDefaultableAttribute), OperationKind.DefaultValue);
36+
}
37+
}
38+
39+
private static Action<OperationAnalysisContext> AnalyzeDefaultValueOperation(INamedTypeSymbol nonDefaultableAttribute)
40+
=> context =>
41+
{
42+
var operation = (IDefaultValueOperation)context.Operation;
43+
if (operation.Type is { } type && type.GetAttributes().Any(IsAttribute(nonDefaultableAttribute)))
44+
{
45+
context.ReportDiagnostic(Diagnostic.Create(
46+
DoNotUseDefault,
47+
operation.Syntax.GetLocation(),
48+
messageArgs: type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)));
49+
}
50+
};
51+
52+
private static Func<AttributeData, bool> IsAttribute(INamedTypeSymbol attributeClass)
53+
=> attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeClass);
54+
}

Funcky.Test/Monads/EitherTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using FsCheck;
23
using FsCheck.Xunit;
34
using Funcky.FsCheck;
@@ -46,6 +47,7 @@ public void CreateEitherRightAndMatchCorrectly()
4647
}
4748

4849
[Fact]
50+
[SuppressMessage("Funcky", "λ1009:Do not use default to instantiate this type", Justification = "Intentionally creating an invalid instance.")]
4951
public void MatchThrowsWhenEitherIsCreatedWithDefault()
5052
{
5153
var value = default(Either<string, int>);

Funcky.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ EndProject
1717
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build Config", "Build Config", "{DD8F8450-BE23-4D6B-9C5C-7AED0ABB7531}"
1818
ProjectSection(SolutionItems) = preProject
1919
Directory.Build.props = Directory.Build.props
20+
Directory.Packages.props = Directory.Packages.props
2021
FrameworkFeatureConstants.props = FrameworkFeatureConstants.props
2122
global.json = global.json
2223
GlobalUsings.props = GlobalUsings.props
2324
GlobalUsings.Test.props = GlobalUsings.Test.props
2425
NuGet.config = NuGet.config
25-
Directory.Packages.props = Directory.Packages.props
2626
EndProjectSection
2727
EndProject
2828
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Funcky.Xunit", "Funcky.Xunit\Funcky.Xunit.csproj", "{F2E98B0D-CC17-4576-89DE-065FF475BE6E}"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace Funcky.CodeAnalysis;
2+
3+
/// <summary>Structs annotated with this attribute should not be instantiated with <see langword="default"/>.</summary>
4+
[AttributeUsage(AttributeTargets.Struct)]
5+
internal sealed class NonDefaultableAttribute : Attribute;

Funcky/EitherOrBoth.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Funcky;
77
/// EitherOrBoth values constructed using <c>default</c> are in an invalid state.
88
/// Any attempt to perform actions on such a value will throw a <see cref="NotSupportedException"/>.
99
/// </remarks>
10+
[NonDefaultable]
1011
public readonly struct EitherOrBoth<TLeft, TRight> : IEquatable<EitherOrBoth<TLeft, TRight>>
1112
where TLeft : notnull
1213
where TRight : notnull

Funcky/Monads/Either/Either.Core.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Funcky.Monads;
77
/// Either values constructed using <c>default</c> are in an invalid state.
88
/// Any attempt to perform actions on such a value will throw a <see cref="NotSupportedException"/>.
99
/// </remarks>
10+
[NonDefaultable]
1011
public readonly partial struct Either<TLeft, TRight> : IEquatable<Either<TLeft, TRight>>
1112
where TLeft : notnull
1213
where TRight : notnull

Funcky/Monads/Result/Result.Core.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace Funcky.Monads;
1414

15+
[NonDefaultable]
1516
public readonly partial struct Result<TValidResult> : IEquatable<Result<TValidResult>>
1617
where TValidResult : notnull
1718
{

0 commit comments

Comments
 (0)