Skip to content

Commit

Permalink
Merge pull request #840 from polyadic/disallow-option-list-pattern-me…
Browse files Browse the repository at this point in the history
…mbers
  • Loading branch information
bash authored Feb 4, 2025
2 parents 1b5140a + 53036bf commit b63b6fb
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 6 deletions.
76 changes: 76 additions & 0 deletions Funcky.Analyzers/Funcky.Analyzers.Test/SyntaxSupportOnlyTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Microsoft.CodeAnalysis.Testing;
using Xunit;
using VerifyCS = Funcky.Analyzers.Test.CSharpAnalyzerVerifier<Funcky.BuiltinAnalyzers.SyntaxSupportOnlyAnalyzer>;

namespace Funcky.Analyzers.Test;

public sealed class SyntaxSupportOnlyTest
{
// language=csharp
private const string AttributeSource =
"""
namespace Funcky.CodeAnalysis
{
#pragma warning disable CS9113 // Parameter is unread.
[System.AttributeUsage(System.AttributeTargets.Property)]
internal sealed class SyntaxSupportOnlyAttribute(string syntaxFeature) : System.Attribute;
#pragma warning restore CS9113 // Parameter is unread.
}
""";

[Fact]
public async Task DisallowsUseOfPropertiesAnnotatedWithAttribute()
{
// language=csharp
const string inputCode =
"""
public static class C
{
public static void M()
{
var option = new Option();
_ = option.Count;
}
}
public class Option
{
[Funcky.CodeAnalysis.SyntaxSupportOnly("list pattern")]
public int Count => 0;
}
""";

DiagnosticResult[] expectedDiagnostics = [
VerifyCS.Diagnostic().WithSpan(6, 13, 6, 25).WithArguments("property", "list pattern"),
];
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + AttributeSource, expectedDiagnostics);
}

[Fact]
public async Task DisallowsUseOfIndexersAnnotatedWithAttribute()
{
// language=csharp
const string inputCode =
"""
public static class C
{
public static void M()
{
var option = new Option();
_ = option[0];
}
}
public class Option
{
[Funcky.CodeAnalysis.SyntaxSupportOnly("foo")]
public int this[int index] => 0;
}
""";

DiagnosticResult[] expectedDiagnostics = [
VerifyCS.Diagnostic().WithSpan(6, 13, 6, 22).WithArguments("property", "foo"),
];
await VerifyCS.VerifyAnalyzerAsync(inputCode + Environment.NewLine + AttributeSource, expectedDiagnostics);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
λ0003 | Funcky | Error | SyntaxSupportOnlyAnalyzer

### Removed Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="PolySharp" PrivateAssets="all" />
<!-- We use 2.7.0 for compatibility with VS 2019 and .NET Core SDK 3.x.
See https://docs.microsoft.com/en-us/visualstudio/extensibility/roslyn-version-support for VS compatibility.-->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" VersionOverride="3.7.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Funcky.BuiltinAnalyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class SyntaxSupportOnlyAnalyzer : DiagnosticAnalyzer
{
private const string AttributeFullName = "Funcky.CodeAnalysis.SyntaxSupportOnlyAttribute";

private static readonly DiagnosticDescriptor SyntaxSupportOnly = new(
id: "λ0003",
title: "Member is not intended for direct usage",
messageFormat: "This {0} exists only to support the {1} syntax, do not use it directly",
category: nameof(Funcky),
DiagnosticSeverity.Error,
isEnabledByDefault: true,
customTags: [WellKnownDiagnosticTags.NotConfigurable]);

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

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

private static void OnCompilationStart(CompilationStartAnalysisContext context)
{
if (context.Compilation.GetTypeByMetadataName(AttributeFullName) is { } attributeType)
{
context.RegisterOperationAction(AnalyzePropertyReference(new AttributeType(attributeType)), OperationKind.PropertyReference);
}
}

private static Action<OperationAnalysisContext> AnalyzePropertyReference(AttributeType attributeType)
=> context =>
{
var propertyReference = (IPropertyReferenceOperation)context.Operation;
if (HasSyntaxSupportOnlyAttribute(propertyReference.Property, attributeType, out var syntaxFeature))
{
context.ReportDiagnostic(Diagnostic.Create(SyntaxSupportOnly, context.Operation.Syntax.GetLocation(), messageArgs: ["property", syntaxFeature]));
}
};

private static bool HasSyntaxSupportOnlyAttribute(ISymbol symbol, AttributeType attributeType, [NotNullWhen((true))] out string? syntaxFeature)
{
syntaxFeature = null;
return symbol.GetAttributes().FirstOrDefault(IsAttribute(attributeType.Value)) is { } attributeData
&& attributeData.ConstructorArguments is [{ Value: string syntaxFeatureValue }]
&& (syntaxFeature = syntaxFeatureValue) is var _;
}

private static Func<AttributeData, bool> IsAttribute(INamedTypeSymbol attributeClass)
=> attributeData
=> SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, attributeClass);

private sealed record AttributeType(INamedTypeSymbol Value);
}
3 changes: 0 additions & 3 deletions Funcky.Test/Monads/Option.ListPatternTest.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#pragma warning disable SA1010 // list patterns are not supported yet
namespace Funcky.Test.Monads;

#if SYSTEM_INDEX_SUPPORTED
public sealed partial class OptionTest
{
[Fact]
Expand Down Expand Up @@ -69,4 +67,3 @@ public void OptionNoneCanBeTestedWithAListPatternInAnIf()
}
}
}
#endif
9 changes: 9 additions & 0 deletions Funcky/CodeAnalysis/SyntaxSupportOnlyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Funcky.CodeAnalysis;

#pragma warning disable CS9113 // Parameter is unread.
[AttributeUsage(AttributeTargets.Property)]
internal sealed class SyntaxSupportOnlyAttribute(string syntaxFeature) : Attribute
#pragma warning restore CS9113 // Parameter is unread.
{
public const string ListPattern = "list pattern";
}
11 changes: 8 additions & 3 deletions Funcky/Monads/Option/Option.ListPattern.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel;
using System.Diagnostics;
using static System.Diagnostics.DebuggerBrowsableState;
using Funcky.CodeAnalysis;

namespace Funcky.Monads;

Expand All @@ -10,14 +11,18 @@ public readonly partial struct Option<TItem>
private const int SomeLength = 1;

[Pure]
[DebuggerBrowsable(Never)]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[EditorBrowsable(EditorBrowsableState.Never)]
[SyntaxSupportOnly(SyntaxSupportOnlyAttribute.ListPattern)]
public int Count
=> _hasItem
? SomeLength
: NoneLength;

[Pure]
[DebuggerBrowsable(Never)]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[EditorBrowsable(EditorBrowsableState.Never)]
[SyntaxSupportOnly(SyntaxSupportOnlyAttribute.ListPattern)]
public TItem this[int index]
=> _hasItem && index is 0
? _item
Expand Down

0 comments on commit b63b6fb

Please sign in to comment.