From 8e5607ea766dacc90f4640944da9811c0ea72c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 4 Feb 2025 10:51:09 +0100 Subject: [PATCH 1/3] Add analyzer to disallow use of certain members --- .../SyntaxSupportOnlyTest.cs | 76 +++++++++++++++++++ .../AnalyzerReleases.Unshipped.md | 6 ++ .../Funcky.BuiltinAnalyzers.csproj | 1 + .../SyntaxSupportOnlyAnalyzer.cs | 63 +++++++++++++++ .../SyntaxSupportOnlyAttribute.cs | 9 +++ 5 files changed, 155 insertions(+) create mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/SyntaxSupportOnlyTest.cs create mode 100644 Funcky.Analyzers/Funcky.BuiltinAnalyzers/SyntaxSupportOnlyAnalyzer.cs create mode 100644 Funcky/CodeAnalysis/SyntaxSupportOnlyAttribute.cs diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/SyntaxSupportOnlyTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/SyntaxSupportOnlyTest.cs new file mode 100644 index 00000000..2f0de2e4 --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/SyntaxSupportOnlyTest.cs @@ -0,0 +1,76 @@ +using Microsoft.CodeAnalysis.Testing; +using Xunit; +using VerifyCS = Funcky.Analyzers.Test.CSharpAnalyzerVerifier; + +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); + } +} diff --git a/Funcky.Analyzers/Funcky.BuiltinAnalyzers/AnalyzerReleases.Unshipped.md b/Funcky.Analyzers/Funcky.BuiltinAnalyzers/AnalyzerReleases.Unshipped.md index ba891959..3425000c 100644 --- a/Funcky.Analyzers/Funcky.BuiltinAnalyzers/AnalyzerReleases.Unshipped.md +++ b/Funcky.Analyzers/Funcky.BuiltinAnalyzers/AnalyzerReleases.Unshipped.md @@ -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 --------|----------|----------|------- diff --git a/Funcky.Analyzers/Funcky.BuiltinAnalyzers/Funcky.BuiltinAnalyzers.csproj b/Funcky.Analyzers/Funcky.BuiltinAnalyzers/Funcky.BuiltinAnalyzers.csproj index 89f3abba..971dd3eb 100644 --- a/Funcky.Analyzers/Funcky.BuiltinAnalyzers/Funcky.BuiltinAnalyzers.csproj +++ b/Funcky.Analyzers/Funcky.BuiltinAnalyzers/Funcky.BuiltinAnalyzers.csproj @@ -7,6 +7,7 @@ true + diff --git a/Funcky.Analyzers/Funcky.BuiltinAnalyzers/SyntaxSupportOnlyAnalyzer.cs b/Funcky.Analyzers/Funcky.BuiltinAnalyzers/SyntaxSupportOnlyAnalyzer.cs new file mode 100644 index 00000000..a5966ea8 --- /dev/null +++ b/Funcky.Analyzers/Funcky.BuiltinAnalyzers/SyntaxSupportOnlyAnalyzer.cs @@ -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 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 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 IsAttribute(INamedTypeSymbol attributeClass) + => attributeData + => SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, attributeClass); + + private sealed record AttributeType(INamedTypeSymbol Value); +} diff --git a/Funcky/CodeAnalysis/SyntaxSupportOnlyAttribute.cs b/Funcky/CodeAnalysis/SyntaxSupportOnlyAttribute.cs new file mode 100644 index 00000000..9ca07cd2 --- /dev/null +++ b/Funcky/CodeAnalysis/SyntaxSupportOnlyAttribute.cs @@ -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"; +} From 4ef2cc846cfde212c3a17337874bb5e0b30a6b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 4 Feb 2025 10:51:31 +0100 Subject: [PATCH 2/3] Disallow use of `Count` and indexer on `Option` --- Funcky/Monads/Option/Option.ListPattern.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Funcky/Monads/Option/Option.ListPattern.cs b/Funcky/Monads/Option/Option.ListPattern.cs index 55312f40..05c2a3eb 100644 --- a/Funcky/Monads/Option/Option.ListPattern.cs +++ b/Funcky/Monads/Option/Option.ListPattern.cs @@ -1,5 +1,6 @@ +using System.ComponentModel; using System.Diagnostics; -using static System.Diagnostics.DebuggerBrowsableState; +using Funcky.CodeAnalysis; namespace Funcky.Monads; @@ -10,14 +11,18 @@ public readonly partial struct Option 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 From 53036bf0787d2c0878034a6bc88c8f5598137d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tau=20G=C3=A4rtli?= Date: Tue, 4 Feb 2025 10:51:53 +0100 Subject: [PATCH 3/3] Remove unnecessary condition and pragma --- Funcky.Test/Monads/Option.ListPatternTest.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Funcky.Test/Monads/Option.ListPatternTest.cs b/Funcky.Test/Monads/Option.ListPatternTest.cs index 3ac55d7a..6cd4db45 100644 --- a/Funcky.Test/Monads/Option.ListPatternTest.cs +++ b/Funcky.Test/Monads/Option.ListPatternTest.cs @@ -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] @@ -69,4 +67,3 @@ public void OptionNoneCanBeTestedWithAListPatternInAnIf() } } } -#endif