From 6c899c17cd1d31bf6ec1e360242d0ab4bbc2b61e Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sun, 28 Jun 2026 10:48:47 -0700 Subject: [PATCH 1/4] Support C# 14 extension members in CsSig Roslyn models each `extension(Receiver) { ... }` block as a nested type with an unspeakable, compiler-generated name. The writer emitted these as nameless `public class { ... }` declarations, producing broken `.cssig` files for projects that use extension members. - Add ExtensionMembers helper isolating the Roslyn-version-sensitive logic (numeric TypeKind.Extension, reflective ExtensionParameter) so the analyzer keeps targeting the older Roslyn baseline. - CsSigWriter now emits proper `extension(receiver) { ... }` blocks (deterministically ordered), rendering the receiver with ref-kind and nullability. - SignatureModel keys extension markers by the fixed `` discriminator plus their receiver type, so distinct blocks with the same receiver are not conflated. - ApiSurface no longer double-counts the implicit implementation methods the compiler synthesises on the host static class; the members are tracked through the extension marker types instead. - Bump the test harness to Roslyn 5.x (Microsoft.CodeAnalysis.Analyzer.Testing 1.1.4, CSharp.Workspaces 5.3.0) and add round-trip + soundness regression tests for extension members. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/CsSig/Analyzer/ApiSurface.cs | 11 +++ src/CsSig/Analyzer/CsSigWriter.cs | 50 ++++++++++- src/CsSig/Analyzer/ExtensionMembers.cs | 60 +++++++++++++ src/CsSig/Analyzer/SignatureModel.cs | 34 +++++++- test/test/CsSigTests.cs | 114 ++++++++++++++++++++++--- test/test/test.csproj | 5 +- 6 files changed, 259 insertions(+), 15 deletions(-) create mode 100644 src/CsSig/Analyzer/ExtensionMembers.cs diff --git a/src/CsSig/Analyzer/ApiSurface.cs b/src/CsSig/Analyzer/ApiSurface.cs index 5ad5d97..e0654ed 100644 --- a/src/CsSig/Analyzer/ApiSurface.cs +++ b/src/CsSig/Analyzer/ApiSurface.cs @@ -126,8 +126,19 @@ private static IEnumerable GetApiMembers(INamedTypeSymbol type) // Implicitly declared members of a record (Equals, GetHashCode, Deconstruct, copy ctor, // positional property accessors, ...). + // + // A static class hosting extension blocks also carries implicit *implementation* methods + // for each extension member (e.g. `get_Empty`, `TryFirst`). Those are implementation + // details: the members themselves are tracked through the extension marker types, so skip + // the implicit-method pass for such classes to avoid double-counting. + bool hostsExtensions = ExtensionMembers.ContainsExtension(type); foreach (var member in type.GetMembers()) { + if (hostsExtensions) + { + break; + } + if (SymbolEqualityComparer.Default.Equals(member, implicitConstructor)) { continue; diff --git a/src/CsSig/Analyzer/CsSigWriter.cs b/src/CsSig/Analyzer/CsSigWriter.cs index 1df6e48..fe86bf3 100644 --- a/src/CsSig/Analyzer/CsSigWriter.cs +++ b/src/CsSig/Analyzer/CsSigWriter.cs @@ -157,7 +157,16 @@ private static void WriteType(IndentingBuilder builder, INamedTypeSymbol type) builder.AppendLine(FormatMember(member)); } - foreach (var nested in Sorted(type.GetMembers().OfType().Where(ApiSurface.IsTrackedApi))) + var nestedTypes = type.GetMembers().OfType().Where(ApiSurface.IsTrackedApi).ToList(); + + // Extension blocks are nested types with an unspeakable name; emit them as + // `extension(Receiver) { ... }` ordered by their header so the output is deterministic. + foreach (var extension in nestedTypes.Where(ExtensionMembers.IsExtension).OrderBy(ExtensionHeader, StringComparer.Ordinal)) + { + WriteExtension(builder, extension); + } + + foreach (var nested in Sorted(nestedTypes.Where(static t => !ExtensionMembers.IsExtension(t)))) { WriteType(builder, nested); } @@ -167,6 +176,45 @@ private static void WriteType(IndentingBuilder builder, INamedTypeSymbol type) builder.AppendLine("}"); } + private static void WriteExtension(IndentingBuilder builder, INamedTypeSymbol extension) + { + builder.AppendLine(ExtensionHeader(extension)); + builder.AppendLine("{"); + builder.Indent(); + + foreach (var member in Sorted(VisibleMembers(extension))) + { + builder.AppendLine(FormatMember(member)); + } + + builder.Dedent(); + builder.AppendLine("}"); + } + + /// The header of an extension block, e.g. extension(int) or + /// extension<T>(T[] source). The receiver is the block's marker type's receiver + /// parameter; its name is emitted only when the source declared one. + private static string ExtensionHeader(INamedTypeSymbol extension) + { + var receiver = ExtensionMembers.Receiver(extension); + var receiverText = receiver is null ? string.Empty : FormatReceiver(receiver); + return $"extension{TypeParameterList(extension)}({receiverText})"; + } + + private static string FormatReceiver(IParameterSymbol receiver) + { + var prefix = receiver.RefKind switch + { + RefKind.Ref => "ref ", + RefKind.Out => "out ", + RefKind.In => "in ", + _ => string.Empty, + }; + + var type = prefix + receiver.Type.ToDisplayString(s_typeFormat); + return receiver.Name.Length == 0 ? type : type + " " + receiver.Name; + } + /// Generates the body-less declaration text of a single member, exactly as it should /// appear inside a type in a .cssig file (no leading indentation, no trailing newline). /// Enum members are rendered as Name = value,. diff --git a/src/CsSig/Analyzer/ExtensionMembers.cs b/src/CsSig/Analyzer/ExtensionMembers.cs new file mode 100644 index 0000000..185f9a3 --- /dev/null +++ b/src/CsSig/Analyzer/ExtensionMembers.cs @@ -0,0 +1,60 @@ +using System.Reflection; +using Microsoft.CodeAnalysis; + +namespace CsSig; + +/// +/// Helpers for C# "extension" members (extension(Receiver) { ... }). Roslyn models an +/// extension block as a nested type whose is +/// TypeKind.Extension and whose name is an unspeakable, compiler-generated marker derived +/// from the block's contents. The members declared inside the block live on that marker type; the +/// enclosing static class carries only their (implicit) implementations. +/// +internal static class ExtensionMembers +{ + /// + /// The structural name used for an extension marker type in a / + /// . The real metadata name is an unspeakable content hash, so the + /// block is instead identified by this fixed discriminator plus its receiver type. + /// + public const string Name = ""; + + // TypeKind.Extension is newer than the Roslyn baseline this analyzer compiles against, but the + // host compiler reports it at runtime. Reference it by its numeric value (mirroring how + // ApiMember handles RefKind.RefReadOnlyParameter) so the analyzer keeps targeting the older + // Roslyn version. + private const TypeKind ExtensionKind = (TypeKind)14; + + // INamedTypeSymbol.ExtensionParameter (the receiver of an extension block) is likewise newer + // than the baseline, so it is read reflectively. + private static readonly PropertyInfo? s_extensionParameter = + typeof(INamedTypeSymbol).GetProperty("ExtensionParameter"); + + /// Whether is an extension marker type. + public static bool IsExtension(INamedTypeSymbol type) => type.TypeKind == ExtensionKind; + + /// + /// Whether directly contains any extension block (i.e. it is a static + /// class hosting extension(...) { ... } members). Such a class also carries the implicit + /// implementation methods of those members, which are not part of the signature surface. + /// + public static bool ContainsExtension(INamedTypeSymbol type) + { + foreach (var member in type.GetTypeMembers()) + { + if (IsExtension(member)) + { + return true; + } + } + + return false; + } + + /// + /// The receiver parameter of an extension block (e.g. the int in extension(int)), + /// or when it cannot be determined. + /// + public static IParameterSymbol? Receiver(INamedTypeSymbol type) => + s_extensionParameter?.GetValue(type) as IParameterSymbol; +} diff --git a/src/CsSig/Analyzer/SignatureModel.cs b/src/CsSig/Analyzer/SignatureModel.cs index 5a8cbc3..683dbf6 100644 --- a/src/CsSig/Analyzer/SignatureModel.cs +++ b/src/CsSig/Analyzer/SignatureModel.cs @@ -70,6 +70,20 @@ public static TypeRef From(ITypeSymbol type) case INamedTypeSymbol named: var container = named.ContainingType is { } containingType ? From(containingType) : null; var @namespace = container is null ? NamespaceName(named.ContainingNamespace) : string.Empty; + + if (ExtensionMembers.IsExtension(named)) + { + // An extension block's metadata name is an unspeakable content hash that depends + // on its members, so two compilations agree only when every member agrees. + // Identify it structurally by its receiver type instead, so members are paired + // by the receiver they extend, independent of their sibling members. + var receiver = ExtensionMembers.Receiver(named); + var receiverArgs = receiver is null + ? default + : EqArray.From(new[] { From(receiver.Type) }); + return new Named(@namespace, container, ExtensionMembers.Name, receiverArgs); + } + return new Named( @namespace, container, @@ -320,8 +334,13 @@ public static ApiMember From(ISymbol symbol) { case INamedTypeSymbol named: { + // Extension blocks share an unspeakable empty name; key them by their receiver so + // two blocks that extend different receivers are distinct and members never collide. + var isExtension = ExtensionMembers.IsExtension(named); + var name = isExtension ? ExtensionMembers.Name : named.Name; + var typeParameters = isExtension ? ExtensionReceiverKey(named) : default; var identity = new MemberIdentity( - ApiMemberKind.Type, @namespace, containingType, named.Name, named.Arity, default); + ApiMemberKind.Type, @namespace, containingType, name, named.Arity, typeParameters); var common = new CommonTypeAspects(flags); return new ApiMember(identity, new SourceMember.Type(common), new BinaryMember.Type(common)); } @@ -402,6 +421,19 @@ private static ParamModifiers ParamModifiersFrom(IParameterSymbol parameter) private static RefKind BinaryRefKind(RefKind refKind) => refKind == RefReadOnlyParameter ? RefKind.In : refKind; + /// + /// The identity contribution of an extension block: its receiver parameter, encoded as a single + /// , so that distinguishes blocks by the + /// receiver they extend (the receiver may reference the block's own type parameters). + /// + private static EqArray ExtensionReceiverKey(INamedTypeSymbol extension) + { + var receiver = ExtensionMembers.Receiver(extension); + return receiver is null + ? default + : EqArray.From(new[] { new ParamKey(TypeRef.From(receiver.Type), BinaryRefKind(receiver.RefKind)) }); + } + private static string FormatConstant(object? value) => value is null ? "null" : Convert.ToString(value, CultureInfo.InvariantCulture) ?? "null"; } diff --git a/test/test/CsSigTests.cs b/test/test/CsSigTests.cs index ba17761..41b1b59 100644 --- a/test/test/CsSigTests.cs +++ b/test/test/CsSigTests.cs @@ -576,6 +576,95 @@ public sealed class Nested """); } + [Fact] + public async Task RoundTripExtensionMembers() + { + // C# 14 extension blocks: each `extension(Receiver) { ... }` is modelled by Roslyn as a + // nested type with an unspeakable name. The writer must emit it as an `extension(...)` block + // (not a nameless `class`), and the round-trip must produce no diagnostics. + await AssertRoundTripsAsync(""" + namespace N + { + public static class Ext + { + extension(int[]) + { + public static string Describe => "ints"; + } + extension(T[] source) + { + public int Count2 => source.Length; + public bool TryFirst(out T value) { value = default!; return false; } + public static T[] Empty => System.Array.Empty(); + } + } + } + """, nullable: true, languageVersion: LanguageVersion.Preview); + } + + [Fact] + public async Task ExtensionMemberMissingFromProjectReported() + { + var source = """ + namespace N; + public static class Ext + { + extension(T[] source) + { + public int Count2 => source.Length; + } + } + """; + var sig = """ + namespace N + { + public static class Ext + { + extension(T[] source) + { + public int Count2 { get; } + public static int Bogus { get; } + } + } + } + """; + // `Bogus` is declared in the signature but absent from the project: exactly one report. + var diagnostic = Assert.Single(await RunPreviewAsync(source, sig)); + Assert.Equal("CSSIG001", diagnostic.Id); + } + + [Fact] + public async Task ExtensionMemberMissingFromSignatureReported() + { + var source = """ + namespace N; + public static class Ext + { + extension(T[] source) + { + public int Count2 => source.Length; + public static T[] Empty => System.Array.Empty(); + } + } + """; + var sig = """ + namespace N + { + public static class Ext + { + extension(T[] source) + { + public int Count2 { get; } + } + } + } + """; + // `Empty` exists in the project but is not declared: exactly one report (no double-count + // from the implicit implementation method the compiler synthesises on the static class). + var diagnostic = Assert.Single(await RunPreviewAsync(source, sig)); + Assert.Equal("CSSIG002", diagnostic.Id); + } + [Fact] public async Task RoundTripFunctionPointerAndVolatile() { @@ -593,7 +682,8 @@ public unsafe class C /// Generates a .cssig from and asserts that feeding it /// back through the analyzer reports no diagnostics. - private static async Task AssertRoundTripsAsync(string source, bool nullable = false) + private static async Task AssertRoundTripsAsync( + string source, bool nullable = false, LanguageVersion languageVersion = LanguageVersion.Default) { var references = await ReferenceAssemblies.Net.Net60.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true); @@ -604,14 +694,12 @@ private static async Task AssertRoundTripsAsync(string source, bool nullable = f var compilation = CSharpCompilation.Create( "TestProject", - new[] { CSharpSyntaxTree.ParseText(source, path: "Test.cs") }, + new[] { CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(languageVersion), path: "Test.cs") }, references, compilationOptions); var generated = CsSigWriter.Write(compilation); - var diagnostics = nullable - ? await RunNullableAsync(source, generated) - : await RunAsync(source, generated); + var diagnostics = await RunCoreAsync(source, equivalence: null, nullable, languageVersion, generated); Assert.Empty(diagnostics); } @@ -1082,24 +1170,28 @@ private sealed class CollectedDiagnosticProvider : FixAllContext.DiagnosticProvi } private static Task> RunAsync(string source, params string[] signatureFiles) - => RunCoreAsync(source, equivalence: null, nullable: false, signatureFiles); + => RunCoreAsync(source, equivalence: null, nullable: false, LanguageVersion.Default, signatureFiles); + + private static Task> RunPreviewAsync(string source, params string[] signatureFiles) + => RunCoreAsync(source, equivalence: null, nullable: true, LanguageVersion.Preview, signatureFiles); private static Task> RunNullableAsync(string source, params string[] signatureFiles) - => RunCoreAsync(source, equivalence: null, nullable: true, signatureFiles); + => RunCoreAsync(source, equivalence: null, nullable: true, LanguageVersion.Default, signatureFiles); private static Task> RunWithEquivalenceAsync( string source, string? equivalence, params string[] signatureFiles) - => RunCoreAsync(source, equivalence, nullable: false, signatureFiles); + => RunCoreAsync(source, equivalence, nullable: false, LanguageVersion.Default, signatureFiles); private static Task> RunNullableWithEquivalenceAsync( string source, string? equivalence, params string[] signatureFiles) - => RunCoreAsync(source, equivalence, nullable: true, signatureFiles); + => RunCoreAsync(source, equivalence, nullable: true, LanguageVersion.Default, signatureFiles); private static async Task> RunCoreAsync( - string source, string? equivalence, bool nullable, params string[] signatureFiles) + string source, string? equivalence, bool nullable, LanguageVersion languageVersion, params string[] signatureFiles) { var references = await ReferenceAssemblies.Net.Net60.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + var parseOptions = new CSharpParseOptions(languageVersion); var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true); if (nullable) { @@ -1108,7 +1200,7 @@ private static async Task> RunCoreAsync( var compilation = CSharpCompilation.Create( "TestProject", - new[] { CSharpSyntaxTree.ParseText(source, path: "Test.cs") }, + new[] { CSharpSyntaxTree.ParseText(source, parseOptions, path: "Test.cs") }, references, compilationOptions); diff --git a/test/test/test.csproj b/test/test/test.csproj index 4b9fe94..5cf9705 100644 --- a/test/test/test.csproj +++ b/test/test/test.csproj @@ -11,8 +11,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + + From ea857d176d79cfb85353dc63c015cc73f31a9ddf Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sun, 28 Jun 2026 10:55:31 -0700 Subject: [PATCH 2/4] Target Roslyn 5.0 and use extension APIs directly Now that the analyzer requires C# 14 extension support anyway, bump the analyzer/code-fix projects from the Microsoft.CodeAnalysis 4.0 baseline to 5.0 and drop the version-compatibility workarounds: - ExtensionMembers uses INamedTypeSymbol.IsExtension / ExtensionParameter and TypeKind.Extension directly instead of numeric enum values and reflection. - SignatureModel uses RefKind.RefReadOnlyParameter directly instead of the numeric (RefKind)4 fallback. - Suppress RS1035/RS1037 (newly enforced by the updated analyzer-rules package) via a global config: RS1035 only fires on the third-party IndentingBuilder source, and RS1037 is an orthogonal optimisation hint. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/CsSig/Analyzer/AnalyzerRules.globalconfig | 13 ++++++++++ src/CsSig/Analyzer/ExtensionMembers.cs | 25 +++++-------------- src/CsSig/Analyzer/SignatureModel.cs | 9 ++----- .../Analyzer/StaticCs.CsSig.Analyzers.csproj | 3 ++- .../CodeFixes/StaticCs.CsSig.CodeFixes.csproj | 2 +- 5 files changed, 24 insertions(+), 28 deletions(-) create mode 100644 src/CsSig/Analyzer/AnalyzerRules.globalconfig diff --git a/src/CsSig/Analyzer/AnalyzerRules.globalconfig b/src/CsSig/Analyzer/AnalyzerRules.globalconfig new file mode 100644 index 0000000..fa6ff4e --- /dev/null +++ b/src/CsSig/Analyzer/AnalyzerRules.globalconfig @@ -0,0 +1,13 @@ +is_global = true + +# RS1035: 'Environment' is banned for use by analyzers. The only use is Environment.NewLine inside +# the third-party StaticCS.IndentingBuilder source package, which is compiled into this assembly and +# cannot be modified here. The .cssig writer normalises line endings through the host, so this does +# not affect determinism of the analyzer's diagnostics. +dotnet_diagnostic.RS1035.severity = none + +# RS1037: suggests adding the "CompilationEnd" custom tag to descriptors reported from the +# compilation action. These diagnostics are intentionally whole-compilation (the public surface can +# only be compared once every symbol is known); the tag is an optimisation hint that is orthogonal +# to this change. +dotnet_diagnostic.RS1037.severity = none diff --git a/src/CsSig/Analyzer/ExtensionMembers.cs b/src/CsSig/Analyzer/ExtensionMembers.cs index 185f9a3..4e75086 100644 --- a/src/CsSig/Analyzer/ExtensionMembers.cs +++ b/src/CsSig/Analyzer/ExtensionMembers.cs @@ -1,4 +1,3 @@ -using System.Reflection; using Microsoft.CodeAnalysis; namespace CsSig; @@ -6,9 +5,9 @@ namespace CsSig; /// /// Helpers for C# "extension" members (extension(Receiver) { ... }). Roslyn models an /// extension block as a nested type whose is -/// TypeKind.Extension and whose name is an unspeakable, compiler-generated marker derived -/// from the block's contents. The members declared inside the block live on that marker type; the -/// enclosing static class carries only their (implicit) implementations. +/// and whose name is an unspeakable, compiler-generated marker +/// derived from the block's contents. The members declared inside the block live on that marker +/// type; the enclosing static class carries only their (implicit) implementations. /// internal static class ExtensionMembers { @@ -19,19 +18,8 @@ internal static class ExtensionMembers /// public const string Name = ""; - // TypeKind.Extension is newer than the Roslyn baseline this analyzer compiles against, but the - // host compiler reports it at runtime. Reference it by its numeric value (mirroring how - // ApiMember handles RefKind.RefReadOnlyParameter) so the analyzer keeps targeting the older - // Roslyn version. - private const TypeKind ExtensionKind = (TypeKind)14; - - // INamedTypeSymbol.ExtensionParameter (the receiver of an extension block) is likewise newer - // than the baseline, so it is read reflectively. - private static readonly PropertyInfo? s_extensionParameter = - typeof(INamedTypeSymbol).GetProperty("ExtensionParameter"); - /// Whether is an extension marker type. - public static bool IsExtension(INamedTypeSymbol type) => type.TypeKind == ExtensionKind; + public static bool IsExtension(INamedTypeSymbol type) => type.IsExtension; /// /// Whether directly contains any extension block (i.e. it is a static @@ -42,7 +30,7 @@ public static bool ContainsExtension(INamedTypeSymbol type) { foreach (var member in type.GetTypeMembers()) { - if (IsExtension(member)) + if (member.IsExtension) { return true; } @@ -55,6 +43,5 @@ public static bool ContainsExtension(INamedTypeSymbol type) /// The receiver parameter of an extension block (e.g. the int in extension(int)), /// or when it cannot be determined. /// - public static IParameterSymbol? Receiver(INamedTypeSymbol type) => - s_extensionParameter?.GetValue(type) as IParameterSymbol; + public static IParameterSymbol? Receiver(INamedTypeSymbol type) => type.ExtensionParameter; } diff --git a/src/CsSig/Analyzer/SignatureModel.cs b/src/CsSig/Analyzer/SignatureModel.cs index 683dbf6..2575e27 100644 --- a/src/CsSig/Analyzer/SignatureModel.cs +++ b/src/CsSig/Analyzer/SignatureModel.cs @@ -407,19 +407,14 @@ private static ParamModifiers ParamModifiersFrom(IParameterSymbol parameter) => (parameter.IsParams ? ParamModifiers.Params : ParamModifiers.None) | (parameter.IsThis ? ParamModifiers.This : ParamModifiers.None) | (parameter.HasExplicitDefaultValue ? ParamModifiers.Optional : ParamModifiers.None) - | (parameter.RefKind == RefReadOnlyParameter ? ParamModifiers.RefReadOnly : ParamModifiers.None); - - // RefKind.RefReadOnlyParameter (C# 12) is not defined in the Roslyn baseline this analyzer - // compiles against, but the host compiler reports it at runtime. Reference it by its numeric - // value so the analyzer keeps targeting the older Roslyn version. - private const RefKind RefReadOnlyParameter = (RefKind)4; + | (parameter.RefKind == RefKind.RefReadOnlyParameter ? ParamModifiers.RefReadOnly : ParamModifiers.None); // `in` and `ref readonly` parameters share the same binary calling convention (both an // `in`-flagged byref with a `modreq(InAttribute)`); `ref readonly` only adds a source-level // `RequiresLocationAttribute`. Identity therefore pairs them, and the source difference is // carried by ParamModifiers.RefReadOnly so it surfaces as a source-only modification. private static RefKind BinaryRefKind(RefKind refKind) - => refKind == RefReadOnlyParameter ? RefKind.In : refKind; + => refKind == RefKind.RefReadOnlyParameter ? RefKind.In : refKind; /// /// The identity contribution of an extension block: its receiver parameter, encoded as a single diff --git a/src/CsSig/Analyzer/StaticCs.CsSig.Analyzers.csproj b/src/CsSig/Analyzer/StaticCs.CsSig.Analyzers.csproj index 25818b7..9719eb9 100644 --- a/src/CsSig/Analyzer/StaticCs.CsSig.Analyzers.csproj +++ b/src/CsSig/Analyzer/StaticCs.CsSig.Analyzers.csproj @@ -12,7 +12,8 @@ - + + diff --git a/src/CsSig/CodeFixes/StaticCs.CsSig.CodeFixes.csproj b/src/CsSig/CodeFixes/StaticCs.CsSig.CodeFixes.csproj index 7df923a..5dc93b5 100644 --- a/src/CsSig/CodeFixes/StaticCs.CsSig.CodeFixes.csproj +++ b/src/CsSig/CodeFixes/StaticCs.CsSig.CodeFixes.csproj @@ -29,7 +29,7 @@ - + From 55e651d0119bd6102ca7eb1d5a060c80e900e1f0 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sun, 28 Jun 2026 11:20:09 -0700 Subject: [PATCH 3/4] Reformat and refine CsSig analyzer, code fix, and tests Apply consistent multiline formatting across the CsSig projects and refine the analyzer, recognizer, writer, signature model, and code-fix provider. Switch the test harness from the obsolete XUnitVerifier-based Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit package to Microsoft.CodeAnalysis.CSharp.Analyzer.Testing 1.1.4. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/CsSig/Analyzer/ApiSurface.cs | 132 +++++++----- src/CsSig/Analyzer/CsSigAnalyzer.cs | 122 +++++++---- src/CsSig/Analyzer/CsSigRecognizer.cs | 60 ++++-- src/CsSig/Analyzer/CsSigWriter.cs | 194 ++++++++++-------- src/CsSig/Analyzer/IsExternalInit.cs | 4 +- src/CsSig/Analyzer/SignatureModel.cs | 176 +++++++++++----- .../Analyzer/StaticCs.CsSig.Analyzers.csproj | 2 - src/CsSig/Analyzer/SymbolVisibility.cs | 4 +- src/CsSig/Analyzer/build/StaticCS.CsSig.props | 8 +- src/CsSig/CodeFixes/CsSigCodeFixProvider.cs | 191 ++++++++++++----- .../CodeFixes/StaticCs.CsSig.CodeFixes.csproj | 28 ++- test/test/ClosedTests.cs | 7 +- test/test/SuppressorTest.cs | 3 +- test/test/test.csproj | 2 +- 14 files changed, 623 insertions(+), 310 deletions(-) diff --git a/src/CsSig/Analyzer/ApiSurface.cs b/src/CsSig/Analyzer/ApiSurface.cs index e0654ed..4fec9a6 100644 --- a/src/CsSig/Analyzer/ApiSurface.cs +++ b/src/CsSig/Analyzer/ApiSurface.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; @@ -21,26 +22,23 @@ internal static class ApiSurface /// A readable signature format, used only for diagnostic messages. Equivalence is decided by /// the structural , not by this string. /// - private static readonly SymbolDisplayFormat s_displayFormat = - new( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - propertyStyle: SymbolDisplayPropertyStyle.ShowReadWriteDescriptor, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - memberOptions: - SymbolDisplayMemberOptions.IncludeParameters | - SymbolDisplayMemberOptions.IncludeContainingType | - SymbolDisplayMemberOptions.IncludeExplicitInterface | - SymbolDisplayMemberOptions.IncludeModifiers | - SymbolDisplayMemberOptions.IncludeConstantValue, - parameterOptions: - SymbolDisplayParameterOptions.IncludeExtensionThis | - SymbolDisplayParameterOptions.IncludeParamsRefOut | - SymbolDisplayParameterOptions.IncludeType | - SymbolDisplayParameterOptions.IncludeName | - SymbolDisplayParameterOptions.IncludeDefaultValue, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + private static readonly SymbolDisplayFormat s_displayFormat = new( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + propertyStyle: SymbolDisplayPropertyStyle.ShowReadWriteDescriptor, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + memberOptions: SymbolDisplayMemberOptions.IncludeParameters + | SymbolDisplayMemberOptions.IncludeContainingType + | SymbolDisplayMemberOptions.IncludeExplicitInterface + | SymbolDisplayMemberOptions.IncludeModifiers + | SymbolDisplayMemberOptions.IncludeConstantValue, + parameterOptions: SymbolDisplayParameterOptions.IncludeExtensionThis + | SymbolDisplayParameterOptions.IncludeParamsRefOut + | SymbolDisplayParameterOptions.IncludeType + | SymbolDisplayParameterOptions.IncludeName + | SymbolDisplayParameterOptions.IncludeDefaultValue, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes + ); /// /// Builds a map from the structural signature of every externally visible member declared in @@ -57,26 +55,23 @@ public static Dictionary Collect(IAssemblySymbol assem continue; } - Add(map, type); + Add(type); - foreach (var member in GetApiMembers(type)) - { - Add(map, member); - } + AddApiMembers(type, Add); } return map; - } - private static void Add(Dictionary map, ISymbol symbol) - { - var member = ApiMember.From(symbol); - if (map.ContainsKey(member.Identity)) + void Add(ISymbol symbol) { - return; - } + var member = ApiMember.From(symbol); + if (map.ContainsKey(member.Identity)) + { + return; + } - map.Add(member.Identity, new ApiEntry(member, GetLocation(symbol), GetDisplay(symbol))); + map.Add(member.Identity, new ApiEntry(member, GetLocation(symbol), GetDisplay(symbol))); + } } private static Location GetLocation(ISymbol symbol) @@ -92,7 +87,7 @@ private static Location GetLocation(ISymbol symbol) /// Yields the tracked members of , including implicit /// constructors and implicit record members, mirroring the Public API analyzer. - private static IEnumerable GetApiMembers(INamedTypeSymbol type) + private static void AddApiMembers(INamedTypeSymbol type, Action add) { foreach (var member in type.GetMembers()) { @@ -109,18 +104,24 @@ private static IEnumerable GetApiMembers(INamedTypeSymbol type) if (IsTrackedApi(member)) { - yield return member; + add(member); } } // Implicitly declared (parameterless) constructor. IMethodSymbol? implicitConstructor = null; - if (type is { TypeKind: TypeKind.Class, InstanceConstructors.Length: 1 } or { TypeKind: TypeKind.Struct }) + if ( + type + is { TypeKind: TypeKind.Class, InstanceConstructors.Length: 1 } + or { TypeKind: TypeKind.Struct } + ) { - implicitConstructor = type.InstanceConstructors.FirstOrDefault(static c => c.IsImplicitlyDeclared); + implicitConstructor = type.InstanceConstructors.FirstOrDefault(static c => + c.IsImplicitlyDeclared + ); if (implicitConstructor is not null && IsTrackedApi(implicitConstructor)) { - yield return implicitConstructor; + add(implicitConstructor); } } @@ -144,22 +145,28 @@ private static IEnumerable GetApiMembers(INamedTypeSymbol type) continue; } - if (member is IMethodSymbol { IsImplicitlyDeclared: true } method && IsTrackedApi(method)) + if ( + member is IMethodSymbol { IsImplicitlyDeclared: true } method + && IsTrackedApi(method) + ) { // Skip accessors of explicit (non-implicit) properties: those properties are // tracked through their own accessor callbacks already. Keep accessors that // belong to implicit properties (e.g. record `EqualityContract`). - if (method.MethodKind is not (MethodKind.PropertyGet or MethodKind.PropertySet) || - method is { AssociatedSymbol.IsImplicitlyDeclared: true }) + if ( + method.MethodKind is not (MethodKind.PropertyGet or MethodKind.PropertySet) + || method is { AssociatedSymbol.IsImplicitlyDeclared: true } + ) { - yield return method; + add(method); } } } } - private static IEnumerable AllNamedTypes(INamespaceSymbol root) + private static List AllNamedTypes(INamespaceSymbol root) { + var result = new List(); var stack = new Stack(); stack.Push(root); @@ -174,12 +181,14 @@ private static IEnumerable AllNamedTypes(INamespaceSymbol root stack.Push(ns); break; case INamedTypeSymbol type: - yield return type; + result.Add(type); stack.Push(type); break; } } } + + return result; } /// @@ -197,13 +206,22 @@ public static bool IsTrackedApi(ISymbol symbol) } // Enum constructors are not user-visible API. - if (methodSymbol is { MethodKind: MethodKind.Constructor, ContainingType.TypeKind: TypeKind.Enum }) + if ( + methodSymbol is + { MethodKind: MethodKind.Constructor, ContainingType.TypeKind: TypeKind.Enum } + ) { return false; } // For delegates, only the 'Invoke' method carries the signature. - if (methodSymbol is { ContainingType.TypeKind: TypeKind.Delegate, MethodKind: not MethodKind.DelegateInvoke }) + if ( + methodSymbol is + { + ContainingType.TypeKind: TypeKind.Delegate, + MethodKind: not MethodKind.DelegateInvoke + } + ) { return false; } @@ -228,7 +246,10 @@ public static bool IsTrackedApi(ISymbol symbol) case Accessibility.ProtectedOrInternal: // Protected members are only externally visible if the containing type can // actually be extended outside the assembly. - if (current.ContainingType is not { } container || !CanTypeBeExtended(container)) + if ( + current.ContainingType is not { } container + || !CanTypeBeExtended(container) + ) { return false; } @@ -244,13 +265,16 @@ private static bool CanTypeBeExtended(ITypeSymbol type) { // A type can be extended publicly if it isn't sealed and has a constructor that is not // internal, private, or protected-and-internal. - return !type.IsSealed && - type.GetMembers(InstanceConstructorName).Any(static m => m.DeclaredAccessibility switch - { - Accessibility.Internal or Accessibility.ProtectedAndInternal => false, - Accessibility.Private => false, - _ => true, - }); + return !type.IsSealed + && type.GetMembers(InstanceConstructorName) + .Any(static m => + m.DeclaredAccessibility switch + { + Accessibility.Internal or Accessibility.ProtectedAndInternal => false, + Accessibility.Private => false, + _ => true, + } + ); } /// diff --git a/src/CsSig/Analyzer/CsSigAnalyzer.cs b/src/CsSig/Analyzer/CsSigAnalyzer.cs index cfc6d60..b1c8c20 100644 --- a/src/CsSig/Analyzer/CsSigAnalyzer.cs +++ b/src/CsSig/Analyzer/CsSigAnalyzer.cs @@ -26,7 +26,8 @@ public sealed class CsSigAnalyzer : DiagnosticAnalyzer messageFormat: "The signature '{0}' is declared in a .cssig file but is not part of the project's public API (breaks {1} equivalence)", category: "CsSig", defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + isEnabledByDefault: true + ); private static readonly DiagnosticDescriptor s_missingFromSignature = new( id: DiagId.MissingFromSignature.ToIdString(), @@ -34,7 +35,8 @@ public sealed class CsSigAnalyzer : DiagnosticAnalyzer messageFormat: "The signature '{0}' is part of the project's public API but is not declared in any .cssig file (breaks {1} equivalence)", category: "CsSig", defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + isEnabledByDefault: true + ); private static readonly DiagnosticDescriptor s_signatureFileError = new( id: DiagId.SignatureFileError.ToIdString(), @@ -42,7 +44,8 @@ public sealed class CsSigAnalyzer : DiagnosticAnalyzer messageFormat: "The .cssig file could not be parsed: {0}", category: "CsSig", defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + isEnabledByDefault: true + ); private static readonly DiagnosticDescriptor s_signatureMismatch = new( id: DiagId.SignatureMismatch.ToIdString(), @@ -50,11 +53,17 @@ public sealed class CsSigAnalyzer : DiagnosticAnalyzer messageFormat: "The signature '{0}' is declared in a .cssig file but does not match the project's public API (breaks {1} equivalence)", category: "CsSig", defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + isEnabledByDefault: true + ); public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( - s_missingFromProject, s_missingFromSignature, s_signatureFileError, s_signatureMismatch, CsSigRecognizer.Rule); + s_missingFromProject, + s_missingFromSignature, + s_signatureFileError, + s_signatureMismatch, + CsSigRecognizer.Rule + ); public override void Initialize(AnalysisContext context) { @@ -67,8 +76,10 @@ private static void AnalyzeCompilation(CompilationAnalysisContext context) { var compilation = context.Compilation; - var sigFiles = context.Options.AdditionalFiles - .Where(static f => f.Path.EndsWith(Extension, System.StringComparison.OrdinalIgnoreCase)) + var sigFiles = context + .Options.AdditionalFiles.Where(static f => + f.Path.EndsWith(Extension, System.StringComparison.OrdinalIgnoreCase) + ) .ToImmutableArray(); // Nothing to enforce unless the project declares signatures. When one or more .cssig @@ -78,7 +89,8 @@ private static void AnalyzeCompilation(CompilationAnalysisContext context) return; } - var parseOptions = compilation.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions + var parseOptions = + compilation.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions ?? CSharpParseOptions.Default; var sigTrees = new List(sigFiles.Length); @@ -91,17 +103,25 @@ private static void AnalyzeCompilation(CompilationAnalysisContext context) continue; } - var tree = CSharpSyntaxTree.ParseText(text, parseOptions, path: file.Path, cancellationToken: context.CancellationToken); + var tree = CSharpSyntaxTree.ParseText( + text, + parseOptions, + path: file.Path, + cancellationToken: context.CancellationToken + ); foreach (var diagnostic in tree.GetDiagnostics(context.CancellationToken)) { if (diagnostic.Severity == DiagnosticSeverity.Error) { hadParseError = true; - context.ReportDiagnostic(Diagnostic.Create( - s_signatureFileError, - CsSigLocation.ToExternal(diagnostic.Location, file.Path), - diagnostic.GetMessage())); + context.ReportDiagnostic( + Diagnostic.Create( + s_signatureFileError, + CsSigLocation.ToExternal(diagnostic.Location, file.Path), + diagnostic.GetMessage() + ) + ); } } @@ -126,7 +146,8 @@ private static void AnalyzeCompilation(CompilationAnalysisContext context) "__cssig__", sigTrees, compilation.References, - compilation.Options as CSharpCompilationOptions); + compilation.Options as CSharpCompilationOptions + ); var declared = ApiSurface.Collect(sigCompilation.Assembly); var actual = ApiSurface.Collect(compilation.Assembly); @@ -141,30 +162,40 @@ private static void AnalyzeCompilation(CompilationAnalysisContext context) { // Declared in a .cssig file but missing from the project. An add/remove breaks // whichever equivalence is being enforced. - context.ReportDiagnostic(Diagnostic.Create( - s_missingFromProject, - CsSigLocation.ToExternal(pair.Value.Location, sigFiles[0].Path), - pair.Value.Display, - Describe(mode))); + context.ReportDiagnostic( + Diagnostic.Create( + s_missingFromProject, + CsSigLocation.ToExternal(pair.Value.Location, sigFiles[0].Path), + pair.Value.Display, + Describe(mode) + ) + ); continue; } // Present on both sides: the identities match, so compare the equivalence projections // that are active. A common-aspect change differs in both views, yielding a single // diagnostic labelled with both equivalences. - var sourceDiffers = (mode & Equivalence.Source) != 0 + var sourceDiffers = + (mode & Equivalence.Source) != 0 && !Equals(pair.Value.Member.Source, actualEntry.Member.Source); - var binaryDiffers = (mode & Equivalence.Binary) != 0 + var binaryDiffers = + (mode & Equivalence.Binary) != 0 && !Equals(pair.Value.Member.Binary, actualEntry.Member.Binary); if (sourceDiffers || binaryDiffers) { - context.ReportDiagnostic(Diagnostic.Create( - s_signatureMismatch, - CsSigLocation.ToExternal(pair.Value.Location, sigFiles[0].Path), - pair.Value.Display, - Describe( - (sourceDiffers ? Equivalence.Source : 0) | (binaryDiffers ? Equivalence.Binary : 0)))); + context.ReportDiagnostic( + Diagnostic.Create( + s_signatureMismatch, + CsSigLocation.ToExternal(pair.Value.Location, sigFiles[0].Path), + pair.Value.Display, + Describe( + (sourceDiffers ? Equivalence.Source : 0) + | (binaryDiffers ? Equivalence.Binary : 0) + ) + ) + ); } } @@ -174,11 +205,14 @@ private static void AnalyzeCompilation(CompilationAnalysisContext context) context.CancellationToken.ThrowIfCancellationRequested(); if (!declared.ContainsKey(pair.Key)) { - context.ReportDiagnostic(Diagnostic.Create( - s_missingFromSignature, - pair.Value.Location, - pair.Value.Display, - Describe(mode))); + context.ReportDiagnostic( + Diagnostic.Create( + s_missingFromSignature, + pair.Value.Location, + pair.Value.Display, + Describe(mode) + ) + ); } } } @@ -189,9 +223,12 @@ private static void AnalyzeCompilation(CompilationAnalysisContext context) /// private static Equivalence ReadEquivalence(AnalyzerOptions options) { - if (options.AnalyzerConfigOptionsProvider.GlobalOptions - .TryGetValue("build_property.CsSigEquivalence", out var raw) - && !string.IsNullOrWhiteSpace(raw)) + if ( + options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue( + "build_property.CsSigEquivalence", + out var raw + ) && !string.IsNullOrWhiteSpace(raw) + ) { switch (raw.Trim().ToLowerInvariant()) { @@ -208,13 +245,14 @@ private static Equivalence ReadEquivalence(AnalyzerOptions options) return Equivalence.Both; } - private static string Describe(Equivalence equivalence) => equivalence switch - { - Equivalence.Source => "source", - Equivalence.Binary => "binary", - Equivalence.Both => "source and binary", - _ => "no", - }; + private static string Describe(Equivalence equivalence) => + equivalence switch + { + Equivalence.Source => "source", + Equivalence.Binary => "binary", + Equivalence.Both => "source and binary", + _ => "no", + }; } /// The equivalence relation(s) the analyzer enforces between project and signatures. diff --git a/src/CsSig/Analyzer/CsSigRecognizer.cs b/src/CsSig/Analyzer/CsSigRecognizer.cs index 0aff8bf..2d26eb5 100644 --- a/src/CsSig/Analyzer/CsSigRecognizer.cs +++ b/src/CsSig/Analyzer/CsSigRecognizer.cs @@ -39,13 +39,17 @@ internal static class CsSigRecognizer messageFormat: "{0}", category: "CsSig", defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + isEnabledByDefault: true + ); /// /// Recognizes against the .cssig grammar, yielding a diagnostic /// for every construct that the grammar does not allow. /// - public static IEnumerable Recognize(SyntaxTree tree, CancellationToken cancellationToken = default) + public static IEnumerable Recognize( + SyntaxTree tree, + CancellationToken cancellationToken = default + ) { var walker = new Walker(tree.FilePath); walker.Visit(tree.GetRoot(cancellationToken)); @@ -60,12 +64,17 @@ private sealed class Walker : CSharpSyntaxWalker public List Diagnostics { get; } = new(); - private void Report(Location location, string message) - => Diagnostics.Add(Diagnostic.Create(Rule, CsSigLocation.ToExternal(location, _path), message)); + private void Report(Location location, string message) => + Diagnostics.Add( + Diagnostic.Create(Rule, CsSigLocation.ToExternal(location, _path), message) + ); - private static bool IsAccessibility(SyntaxKind kind) - => kind is SyntaxKind.PublicKeyword or SyntaxKind.PrivateKeyword - or SyntaxKind.ProtectedKeyword or SyntaxKind.InternalKeyword; + private static bool IsAccessibility(SyntaxKind kind) => + kind + is SyntaxKind.PublicKeyword + or SyntaxKind.PrivateKeyword + or SyntaxKind.ProtectedKeyword + or SyntaxKind.InternalKeyword; /// /// Modifiers allowed on a virtualizable member (method, property, indexer, event): the @@ -109,7 +118,8 @@ private void CheckModifiers(SyntaxTokenList modifiers, params SyntaxKind[] allow Report( modifier.GetLocation(), - $"The '{modifier.ValueText}' modifier does not affect the signature and is not allowed in a .cssig file"); + $"The '{modifier.ValueText}' modifier does not affect the signature and is not allowed in a .cssig file" + ); } } @@ -119,14 +129,16 @@ private void RejectBody(BlockSyntax? body, ArrowExpressionClauseSyntax? expressi { Report( body.GetLocation(), - "Member bodies are not allowed in a .cssig file; signatures declare members without an implementation"); + "Member bodies are not allowed in a .cssig file; signatures declare members without an implementation" + ); } if (expressionBody is not null) { Report( expressionBody.GetLocation(), - "Expression bodies are not allowed in a .cssig file; signatures declare members without an implementation"); + "Expression bodies are not allowed in a .cssig file; signatures declare members without an implementation" + ); } } @@ -134,7 +146,12 @@ public override void VisitClassDeclaration(ClassDeclarationSyntax node) { // 'static'/'abstract'/'sealed' all affect the type's signature (instantiation, // extensibility of protected members, virtual dispatch). - CheckModifiers(node.Modifiers, SyntaxKind.StaticKeyword, SyntaxKind.AbstractKeyword, SyntaxKind.SealedKeyword); + CheckModifiers( + node.Modifiers, + SyntaxKind.StaticKeyword, + SyntaxKind.AbstractKeyword, + SyntaxKind.SealedKeyword + ); base.VisitClassDeclaration(node); } @@ -159,7 +176,12 @@ public override void VisitRecordDeclaration(RecordDeclarationSyntax node) } else { - CheckModifiers(node.Modifiers, SyntaxKind.StaticKeyword, SyntaxKind.AbstractKeyword, SyntaxKind.SealedKeyword); + CheckModifiers( + node.Modifiers, + SyntaxKind.StaticKeyword, + SyntaxKind.AbstractKeyword, + SyntaxKind.SealedKeyword + ); } base.VisitRecordDeclaration(node); @@ -193,7 +215,9 @@ public override void VisitOperatorDeclaration(OperatorDeclarationSyntax node) base.VisitOperatorDeclaration(node); } - public override void VisitConversionOperatorDeclaration(ConversionOperatorDeclarationSyntax node) + public override void VisitConversionOperatorDeclaration( + ConversionOperatorDeclarationSyntax node + ) { CheckModifiers(node.Modifiers, SyntaxKind.StaticKeyword); RejectBody(node.Body, node.ExpressionBody); @@ -244,7 +268,12 @@ public override void VisitFieldDeclaration(FieldDeclarationSyntax node) { // 'static' -> ApiMember.IsStatic; 'const' -> the captured constant value; // 'readonly' -> the field's read-only-ness (observable to external writers). - CheckModifiers(node.Modifiers, SyntaxKind.StaticKeyword, SyntaxKind.ConstKeyword, SyntaxKind.ReadOnlyKeyword); + CheckModifiers( + node.Modifiers, + SyntaxKind.StaticKeyword, + SyntaxKind.ConstKeyword, + SyntaxKind.ReadOnlyKeyword + ); // A field's value is only part of the signature when it is 'const'; any other // initializer is invisible to the comparison. @@ -256,7 +285,8 @@ public override void VisitFieldDeclaration(FieldDeclarationSyntax node) { Report( initializer.GetLocation(), - "A field initializer does not affect the signature and is not allowed in a .cssig file; only 'const' values are part of the signature"); + "A field initializer does not affect the signature and is not allowed in a .cssig file; only 'const' values are part of the signature" + ); } } } diff --git a/src/CsSig/Analyzer/CsSigWriter.cs b/src/CsSig/Analyzer/CsSigWriter.cs index fe86bf3..d30cb8a 100644 --- a/src/CsSig/Analyzer/CsSigWriter.cs +++ b/src/CsSig/Analyzer/CsSigWriter.cs @@ -27,36 +27,34 @@ public static class CsSigWriter globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining, typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - memberOptions: - SymbolDisplayMemberOptions.IncludeParameters | - SymbolDisplayMemberOptions.IncludeType | - SymbolDisplayMemberOptions.IncludeModifiers | - SymbolDisplayMemberOptions.IncludeAccessibility | - SymbolDisplayMemberOptions.IncludeConstantValue | - SymbolDisplayMemberOptions.IncludeRef | - SymbolDisplayMemberOptions.IncludeExplicitInterface, + memberOptions: SymbolDisplayMemberOptions.IncludeParameters + | SymbolDisplayMemberOptions.IncludeType + | SymbolDisplayMemberOptions.IncludeModifiers + | SymbolDisplayMemberOptions.IncludeAccessibility + | SymbolDisplayMemberOptions.IncludeConstantValue + | SymbolDisplayMemberOptions.IncludeRef + | SymbolDisplayMemberOptions.IncludeExplicitInterface, kindOptions: SymbolDisplayKindOptions.IncludeMemberKeyword, propertyStyle: SymbolDisplayPropertyStyle.ShowReadWriteDescriptor, - parameterOptions: - SymbolDisplayParameterOptions.IncludeType | - SymbolDisplayParameterOptions.IncludeName | - SymbolDisplayParameterOptions.IncludeParamsRefOut | - SymbolDisplayParameterOptions.IncludeExtensionThis | - SymbolDisplayParameterOptions.IncludeDefaultValue, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.UseSpecialTypes | - SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers); + parameterOptions: SymbolDisplayParameterOptions.IncludeType + | SymbolDisplayParameterOptions.IncludeName + | SymbolDisplayParameterOptions.IncludeParamsRefOut + | SymbolDisplayParameterOptions.IncludeExtensionThis + | SymbolDisplayParameterOptions.IncludeDefaultValue, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes + | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier + | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers + ); // A bare type reference, fully qualified, for return/parameter/underlying types. private static readonly SymbolDisplayFormat s_typeFormat = new( globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining, typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.UseSpecialTypes | - SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers); + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes + | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier + | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers + ); /// Generates the .cssig text for 's public API. public static string Write(Compilation compilation) => Write(compilation.Assembly); @@ -73,7 +71,9 @@ public static class CsSigWriter /// public static string Write(IAssemblySymbol assembly, ISet? topLevelKeys) { - var byNamespace = new SortedDictionary>(StringComparer.Ordinal); + var byNamespace = new SortedDictionary>( + StringComparer.Ordinal + ); foreach (var type in TopLevelTypes(assembly.GlobalNamespace)) { if (!ApiSurface.IsTrackedApi(type)) @@ -86,7 +86,9 @@ public static string Write(IAssemblySymbol assembly, ISet? topLevelKeys) continue; } - var ns = type.ContainingNamespace is { IsGlobalNamespace: false } n ? n.ToDisplayString() : string.Empty; + var ns = type.ContainingNamespace is { IsGlobalNamespace: false } n + ? n.ToDisplayString() + : string.Empty; if (!byNamespace.TryGetValue(ns, out var list)) { byNamespace[ns] = list = new List(); @@ -145,7 +147,9 @@ private static void WriteType(IndentingBuilder builder, INamedTypeSymbol type) if (type.TypeKind == TypeKind.Enum) { - foreach (var field in type.GetMembers().OfType().Where(f => f.HasConstantValue)) + foreach ( + var field in type.GetMembers().OfType().Where(f => f.HasConstantValue) + ) { builder.AppendLine(FormatMember(field)); } @@ -157,16 +161,27 @@ private static void WriteType(IndentingBuilder builder, INamedTypeSymbol type) builder.AppendLine(FormatMember(member)); } - var nestedTypes = type.GetMembers().OfType().Where(ApiSurface.IsTrackedApi).ToList(); + var nestedTypes = type.GetMembers() + .OfType() + .Where(ApiSurface.IsTrackedApi) + .ToList(); // Extension blocks are nested types with an unspeakable name; emit them as // `extension(Receiver) { ... }` ordered by their header so the output is deterministic. - foreach (var extension in nestedTypes.Where(ExtensionMembers.IsExtension).OrderBy(ExtensionHeader, StringComparer.Ordinal)) + foreach ( + var extension in nestedTypes + .Where(ExtensionMembers.IsExtension) + .OrderBy(ExtensionHeader, StringComparer.Ordinal) + ) { WriteExtension(builder, extension); } - foreach (var nested in Sorted(nestedTypes.Where(static t => !ExtensionMembers.IsExtension(t)))) + foreach ( + var nested in Sorted( + nestedTypes.Where(static t => !ExtensionMembers.IsExtension(t)) + ) + ) { WriteType(builder, nested); } @@ -225,7 +240,8 @@ private static string FormatMember(ISymbol member) return $"{enumField.Name} = {FormatConstant(enumField.ConstantValue)},"; } - var text = member.ToDisplayString(s_memberFormat) + var text = member + .ToDisplayString(s_memberFormat) .Replace("volatile ", string.Empty) .Replace("required ", string.Empty); @@ -255,14 +271,16 @@ private static string TypeHeader(INamedTypeSymbol type) } } - parts.Add(type.TypeKind switch - { - TypeKind.Class => type.IsRecord ? "record" : "class", - TypeKind.Struct => type.IsRecord ? "record struct" : "struct", - TypeKind.Interface => "interface", - TypeKind.Enum => "enum", - _ => "class", - }); + parts.Add( + type.TypeKind switch + { + TypeKind.Class => type.IsRecord ? "record" : "class", + TypeKind.Struct => type.IsRecord ? "record struct" : "struct", + TypeKind.Interface => "interface", + TypeKind.Enum => "enum", + _ => "class", + } + ); parts.Add(type.Name + TypeParameterList(type)); return string.Join(" ", parts); @@ -271,29 +289,36 @@ private static string TypeHeader(INamedTypeSymbol type) private static string DelegateDeclaration(INamedTypeSymbol type) { var invoke = type.DelegateInvokeMethod!; - var @return = (invoke.ReturnsByRef ? "ref " : invoke.ReturnsByRefReadonly ? "ref readonly " : string.Empty) - + invoke.ReturnType.ToDisplayString(s_typeFormat); + var @return = + ( + invoke.ReturnsByRef ? "ref " + : invoke.ReturnsByRefReadonly ? "ref readonly " + : string.Empty + ) + invoke.ReturnType.ToDisplayString(s_typeFormat); return $"{Accessibility(type.DeclaredAccessibility)} delegate {@return} " + $"{type.Name}{TypeParameterList(type)}({FormatParameters(invoke.Parameters)})"; } - private static string TypeParameterList(INamedTypeSymbol type) - => type.TypeParameters.IsEmpty + private static string TypeParameterList(INamedTypeSymbol type) => + type.TypeParameters.IsEmpty ? string.Empty : "<" + string.Join(", ", type.TypeParameters.Select(p => p.Name)) + ">"; - private static string FormatParameters(IEnumerable parameters) - => string.Join(", ", parameters.Select(p => - { - var prefix = p.RefKind switch + private static string FormatParameters(IEnumerable parameters) => + string.Join( + ", ", + parameters.Select(p => { - RefKind.Ref => "ref ", - RefKind.Out => "out ", - RefKind.In => "in ", - _ => string.Empty, - }; - return prefix + p.Type.ToDisplayString(s_typeFormat) + " " + p.Name; - })); + var prefix = p.RefKind switch + { + RefKind.Ref => "ref ", + RefKind.Out => "out ", + RefKind.In => "in ", + _ => string.Empty, + }; + return prefix + p.Type.ToDisplayString(s_typeFormat) + " " + p.Name; + }) + ); /// The members of that should appear in the signature file: /// explicitly declared, externally visible non-type members, excluding accessors (emitted via @@ -307,11 +332,15 @@ private static IEnumerable VisibleMembers(INamedTypeSymbol type) continue; } - if (member is IMethodSymbol + if ( + member is IMethodSymbol { - MethodKind: MethodKind.PropertyGet or MethodKind.PropertySet - or MethodKind.EventAdd or MethodKind.EventRemove, - }) + MethodKind: MethodKind.PropertyGet + or MethodKind.PropertySet + or MethodKind.EventAdd + or MethodKind.EventRemove, + } + ) { continue; } @@ -323,14 +352,15 @@ private static IEnumerable VisibleMembers(INamedTypeSymbol type) } } - private static bool IsVisible(ISymbol member) => member switch - { - // Properties are tracked through their accessors; emit the property if either surfaces. - IPropertySymbol property => - (property.GetMethod is { } getter && ApiSurface.IsTrackedApi(getter)) - || (property.SetMethod is { } setter && ApiSurface.IsTrackedApi(setter)), - _ => ApiSurface.IsTrackedApi(member), - }; + private static bool IsVisible(ISymbol member) => + member switch + { + // Properties are tracked through their accessors; emit the property if either surfaces. + IPropertySymbol property => ( + property.GetMethod is { } getter && ApiSurface.IsTrackedApi(getter) + ) || (property.SetMethod is { } setter && ApiSurface.IsTrackedApi(setter)), + _ => ApiSurface.IsTrackedApi(member), + }; private static IEnumerable TopLevelTypes(INamespaceSymbol root) { @@ -353,8 +383,9 @@ private static IEnumerable TopLevelTypes(INamespaceSymbol root } } - private static IEnumerable Sorted(IEnumerable symbols) where T : ISymbol - => symbols.OrderBy(s => s.ToDisplayString(s_memberFormat), StringComparer.Ordinal); + private static IEnumerable Sorted(IEnumerable symbols) + where T : ISymbol => + symbols.OrderBy(s => s.ToDisplayString(s_memberFormat), StringComparer.Ordinal); /// /// A stable identity for a top-level type — its namespace-qualified name plus generic @@ -364,22 +395,25 @@ private static IEnumerable Sorted(IEnumerable symbols) where T : ISymbo /// public static string TopLevelKey(INamedTypeSymbol type) { - var ns = type.ContainingNamespace is { IsGlobalNamespace: false } n ? n.ToDisplayString() + "." : string.Empty; + var ns = type.ContainingNamespace is { IsGlobalNamespace: false } n + ? n.ToDisplayString() + "." + : string.Empty; var name = type.Arity > 0 ? type.Name + "`" + type.Arity : type.Name; return ns + name; } - private static string Accessibility(Accessibility accessibility) => accessibility switch - { - Microsoft.CodeAnalysis.Accessibility.Public => "public", - Microsoft.CodeAnalysis.Accessibility.Protected => "protected", - Microsoft.CodeAnalysis.Accessibility.ProtectedOrInternal => "protected internal", - Microsoft.CodeAnalysis.Accessibility.ProtectedAndInternal => "private protected", - Microsoft.CodeAnalysis.Accessibility.Internal => "internal", - Microsoft.CodeAnalysis.Accessibility.Private => "private", - _ => "internal", - }; - - private static string FormatConstant(object? value) - => value is null ? "null" : Convert.ToString(value, CultureInfo.InvariantCulture) ?? "null"; + private static string Accessibility(Accessibility accessibility) => + accessibility switch + { + Microsoft.CodeAnalysis.Accessibility.Public => "public", + Microsoft.CodeAnalysis.Accessibility.Protected => "protected", + Microsoft.CodeAnalysis.Accessibility.ProtectedOrInternal => "protected internal", + Microsoft.CodeAnalysis.Accessibility.ProtectedAndInternal => "private protected", + Microsoft.CodeAnalysis.Accessibility.Internal => "internal", + Microsoft.CodeAnalysis.Accessibility.Private => "private", + _ => "internal", + }; + + private static string FormatConstant(object? value) => + value is null ? "null" : Convert.ToString(value, CultureInfo.InvariantCulture) ?? "null"; } diff --git a/src/CsSig/Analyzer/IsExternalInit.cs b/src/CsSig/Analyzer/IsExternalInit.cs index 476a949..2cdc3b1 100644 --- a/src/CsSig/Analyzer/IsExternalInit.cs +++ b/src/CsSig/Analyzer/IsExternalInit.cs @@ -2,7 +2,5 @@ namespace System.Runtime.CompilerServices { // Required so that `record` types and `init` accessors can be used when targeting // netstandard2.0 (which does not ship the IsExternalInit type). - internal static class IsExternalInit - { - } + internal static class IsExternalInit { } } diff --git a/src/CsSig/Analyzer/SignatureModel.cs b/src/CsSig/Analyzer/SignatureModel.cs index 2575e27..176e626 100644 --- a/src/CsSig/Analyzer/SignatureModel.cs +++ b/src/CsSig/Analyzer/SignatureModel.cs @@ -20,15 +20,22 @@ public sealed record Named( string Namespace, TypeRef? ContainingType, string Name, - EqArray TypeArguments) : TypeRef; + EqArray TypeArguments + ) : TypeRef; + public sealed record Array(TypeRef ElementType, int Rank) : TypeRef; + public sealed record Pointer(TypeRef ElementType) : TypeRef; + public sealed record TypeParameter(int Ordinal, bool IsMethodTypeParameter) : TypeRef; + public sealed record FunctionPointer( SignatureCallingConvention CallingConvention, EqArray CallingConventionTypes, ParamKey Return, - EqArray Parameters) : TypeRef; + EqArray Parameters + ) : TypeRef; + public sealed record Dynamic : TypeRef { public static readonly Dynamic Instance = new(); @@ -50,7 +57,8 @@ public static TypeRef From(ITypeSymbol type) case ITypeParameterSymbol typeParameter: return new TypeParameter( typeParameter.Ordinal, - typeParameter.TypeParameterKind == TypeParameterKind.Method); + typeParameter.TypeParameterKind == TypeParameterKind.Method + ); case IDynamicTypeSymbol: return Dynamic.Instance; @@ -59,17 +67,27 @@ public static TypeRef From(ITypeSymbol type) { var signature = functionPointer.Signature; var callingConventionTypes = EqArray.From( - signature.UnmanagedCallingConventionTypes.Select(From)); + signature.UnmanagedCallingConventionTypes.Select(From) + ); var @return = new ParamKey(From(signature.ReturnType), signature.RefKind); var parameters = EqArray.From( - signature.Parameters.Select(p => new ParamKey(From(p.Type), p.RefKind))); + signature.Parameters.Select(p => new ParamKey(From(p.Type), p.RefKind)) + ); return new FunctionPointer( - signature.CallingConvention, callingConventionTypes, @return, parameters); + signature.CallingConvention, + callingConventionTypes, + @return, + parameters + ); } case INamedTypeSymbol named: - var container = named.ContainingType is { } containingType ? From(containingType) : null; - var @namespace = container is null ? NamespaceName(named.ContainingNamespace) : string.Empty; + var container = named.ContainingType is { } containingType + ? From(containingType) + : null; + var @namespace = container is null + ? NamespaceName(named.ContainingNamespace) + : string.Empty; if (ExtensionMembers.IsExtension(named)) { @@ -88,19 +106,22 @@ public static TypeRef From(ITypeSymbol type) @namespace, container, named.Name, - EqArray.From(named.TypeArguments.Select(From))); + EqArray.From(named.TypeArguments.Select(From)) + ); default: // The ITypeSymbol hierarchy above is exhaustive (array, pointer, type parameter, // dynamic, function pointer, named/error). Anything else cannot be modeled // structurally, so fail loudly rather than invent a comparison. throw new ArgumentException( - $"Cannot build a type reference from type kind '{type.TypeKind}'.", nameof(type)); + $"Cannot build a type reference from type kind '{type.TypeKind}'.", + nameof(type) + ); } } - private static string NamespaceName(INamespaceSymbol? @namespace) - => @namespace is null || @namespace.IsGlobalNamespace + private static string NamespaceName(INamespaceSymbol? @namespace) => + @namespace is null || @namespace.IsGlobalNamespace ? string.Empty : @namespace.ToDisplayString(); } @@ -242,7 +263,11 @@ private static void Walk(ITypeSymbol type, ImmutableArray.Builder builder) /// ref readonly vs in), and the nullable annotations of its type. None of these change /// the binary calling convention. /// -internal readonly record struct SourceParam(string Name, ParamModifiers Modifiers, Nullability Nullability); +internal readonly record struct SourceParam( + string Name, + ParamModifiers Modifiers, + Nullability Nullability +); /// /// The identity of an API member: the tuple by which two members from different compilations are @@ -254,7 +279,8 @@ internal sealed record MemberIdentity( TypeRef? ContainingType, string Name, int Arity, - EqArray Parameters); + EqArray Parameters +); /// /// The aspects of a type declaration observable to every consumer (source or binary): @@ -284,7 +310,10 @@ private SourceMember() { } public sealed record Type(CommonTypeAspects Common) : SourceMember; public sealed record Method( - CommonMethodAspects Common, Nullability ReturnNullability, EqArray Parameters) : SourceMember; + CommonMethodAspects Common, + Nullability ReturnNullability, + EqArray Parameters + ) : SourceMember; public sealed record Field(CommonFieldAspects Common, Nullability Nullability) : SourceMember; @@ -321,9 +350,10 @@ internal sealed record ApiMember(MemberIdentity Identity, SourceMember Source, B public static ApiMember From(ISymbol symbol) { var containingType = symbol.ContainingType is { } type ? TypeRef.From(type) : null; - var @namespace = containingType is null && symbol.ContainingNamespace is { IsGlobalNamespace: false } ns - ? ns.ToDisplayString() - : string.Empty; + var @namespace = + containingType is null && symbol.ContainingNamespace is { IsGlobalNamespace: false } ns + ? ns.ToDisplayString() + : string.Empty; // Every flag bit except a field's ReadOnly/HasConstantValue is reported generically by the // symbol: types expose IsAbstract/IsSealed, members expose virtuality, and the rest are @@ -340,51 +370,93 @@ public static ApiMember From(ISymbol symbol) var name = isExtension ? ExtensionMembers.Name : named.Name; var typeParameters = isExtension ? ExtensionReceiverKey(named) : default; var identity = new MemberIdentity( - ApiMemberKind.Type, @namespace, containingType, name, named.Arity, typeParameters); + ApiMemberKind.Type, + @namespace, + containingType, + name, + named.Arity, + typeParameters + ); var common = new CommonTypeAspects(flags); - return new ApiMember(identity, new SourceMember.Type(common), new BinaryMember.Type(common)); + return new ApiMember( + identity, + new SourceMember.Type(common), + new BinaryMember.Type(common) + ); } case IMethodSymbol method: { var keys = EqArray.From( - method.Parameters.Select(p => new ParamKey(TypeRef.From(p.Type), BinaryRefKind(p.RefKind)))); + method.Parameters.Select(p => new ParamKey( + TypeRef.From(p.Type), + BinaryRefKind(p.RefKind) + )) + ); var parameters = EqArray.From( - method.Parameters.Select(p => new SourceParam(p.Name, ParamModifiersFrom(p), Nullability.Of(p.Type)))); + method.Parameters.Select(p => new SourceParam( + p.Name, + ParamModifiersFrom(p), + Nullability.Of(p.Type) + )) + ); var identity = new MemberIdentity( - ApiMemberKind.Method, @namespace, containingType, method.Name, method.Arity, keys); + ApiMemberKind.Method, + @namespace, + containingType, + method.Name, + method.Arity, + keys + ); var common = new CommonMethodAspects( TypeRef.From(method.ReturnType), - flags | (method.IsReadOnly ? MemberFlags.ReadOnly : MemberFlags.None)); + flags | (method.IsReadOnly ? MemberFlags.ReadOnly : MemberFlags.None) + ); return new ApiMember( identity, new SourceMember.Method(common, Nullability.Of(method.ReturnType), parameters), - new BinaryMember.Method(common)); + new BinaryMember.Method(common) + ); } case IFieldSymbol field: { var identity = new MemberIdentity( - ApiMemberKind.Field, @namespace, containingType, field.Name, Arity: 0, default); - flags |= (field.IsReadOnly ? MemberFlags.ReadOnly : MemberFlags.None) + ApiMemberKind.Field, + @namespace, + containingType, + field.Name, + Arity: 0, + default + ); + flags |= + (field.IsReadOnly ? MemberFlags.ReadOnly : MemberFlags.None) | (field.HasConstantValue ? MemberFlags.HasConstantValue : MemberFlags.None); var common = new CommonFieldAspects(TypeRef.From(field.Type), flags); var constant = field.HasConstantValue ? FormatConstant(field.ConstantValue) : null; return new ApiMember( identity, new SourceMember.Field(common, Nullability.Of(field.Type)), - new BinaryMember.Field(common, constant)); + new BinaryMember.Field(common, constant) + ); } case IEventSymbol @event: { var identity = new MemberIdentity( - ApiMemberKind.Event, @namespace, containingType, @event.Name, Arity: 0, default); + ApiMemberKind.Event, + @namespace, + containingType, + @event.Name, + Arity: 0, + default + ); var common = new CommonEventAspects(TypeRef.From(@event.Type), flags); return new ApiMember( identity, new SourceMember.Event(common, Nullability.Of(@event.Type)), - new BinaryMember.Event(common)); + new BinaryMember.Event(common) + ); } default: @@ -392,29 +464,35 @@ public static ApiMember From(ISymbol symbol) // IsTrackedApi/GetApiMembers). Any other kind cannot be credibly modeled or // compared, so fail loudly rather than synthesize a meaningless member. throw new ArgumentException( - $"Cannot build an API member from symbol kind '{symbol.Kind}'.", nameof(symbol)); + $"Cannot build an API member from symbol kind '{symbol.Kind}'.", + nameof(symbol) + ); } } - private static MemberFlags FlagsFrom(ISymbol symbol) - => (symbol.IsStatic ? MemberFlags.Static : MemberFlags.None) - | (symbol.IsVirtual ? MemberFlags.Virtual : MemberFlags.None) - | (symbol.IsAbstract ? MemberFlags.Abstract : MemberFlags.None) - | (symbol.IsOverride ? MemberFlags.Override : MemberFlags.None) - | (symbol.IsSealed ? MemberFlags.Sealed : MemberFlags.None); - - private static ParamModifiers ParamModifiersFrom(IParameterSymbol parameter) - => (parameter.IsParams ? ParamModifiers.Params : ParamModifiers.None) - | (parameter.IsThis ? ParamModifiers.This : ParamModifiers.None) - | (parameter.HasExplicitDefaultValue ? ParamModifiers.Optional : ParamModifiers.None) - | (parameter.RefKind == RefKind.RefReadOnlyParameter ? ParamModifiers.RefReadOnly : ParamModifiers.None); + private static MemberFlags FlagsFrom(ISymbol symbol) => + (symbol.IsStatic ? MemberFlags.Static : MemberFlags.None) + | (symbol.IsVirtual ? MemberFlags.Virtual : MemberFlags.None) + | (symbol.IsAbstract ? MemberFlags.Abstract : MemberFlags.None) + | (symbol.IsOverride ? MemberFlags.Override : MemberFlags.None) + | (symbol.IsSealed ? MemberFlags.Sealed : MemberFlags.None); + + private static ParamModifiers ParamModifiersFrom(IParameterSymbol parameter) => + (parameter.IsParams ? ParamModifiers.Params : ParamModifiers.None) + | (parameter.IsThis ? ParamModifiers.This : ParamModifiers.None) + | (parameter.HasExplicitDefaultValue ? ParamModifiers.Optional : ParamModifiers.None) + | ( + parameter.RefKind == RefKind.RefReadOnlyParameter + ? ParamModifiers.RefReadOnly + : ParamModifiers.None + ); // `in` and `ref readonly` parameters share the same binary calling convention (both an // `in`-flagged byref with a `modreq(InAttribute)`); `ref readonly` only adds a source-level // `RequiresLocationAttribute`. Identity therefore pairs them, and the source difference is // carried by ParamModifiers.RefReadOnly so it surfaces as a source-only modification. - private static RefKind BinaryRefKind(RefKind refKind) - => refKind == RefKind.RefReadOnlyParameter ? RefKind.In : refKind; + private static RefKind BinaryRefKind(RefKind refKind) => + refKind == RefKind.RefReadOnlyParameter ? RefKind.In : refKind; /// /// The identity contribution of an extension block: its receiver parameter, encoded as a single @@ -426,9 +504,11 @@ private static EqArray ExtensionReceiverKey(INamedTypeSymbol extension var receiver = ExtensionMembers.Receiver(extension); return receiver is null ? default - : EqArray.From(new[] { new ParamKey(TypeRef.From(receiver.Type), BinaryRefKind(receiver.RefKind)) }); + : EqArray.From( + new[] { new ParamKey(TypeRef.From(receiver.Type), BinaryRefKind(receiver.RefKind)) } + ); } - private static string FormatConstant(object? value) - => value is null ? "null" : Convert.ToString(value, CultureInfo.InvariantCulture) ?? "null"; + private static string FormatConstant(object? value) => + value is null ? "null" : Convert.ToString(value, CultureInfo.InvariantCulture) ?? "null"; } diff --git a/src/CsSig/Analyzer/StaticCs.CsSig.Analyzers.csproj b/src/CsSig/Analyzer/StaticCs.CsSig.Analyzers.csproj index 9719eb9..7f2c49e 100644 --- a/src/CsSig/Analyzer/StaticCs.CsSig.Analyzers.csproj +++ b/src/CsSig/Analyzer/StaticCs.CsSig.Analyzers.csproj @@ -1,5 +1,4 @@ - netstandard2.0 enable @@ -17,5 +16,4 @@ - diff --git a/src/CsSig/Analyzer/SymbolVisibility.cs b/src/CsSig/Analyzer/SymbolVisibility.cs index 7b2f82e..8dcb036 100644 --- a/src/CsSig/Analyzer/SymbolVisibility.cs +++ b/src/CsSig/Analyzer/SymbolVisibility.cs @@ -52,8 +52,8 @@ public static SymbolVisibility GetResultantVisibility(this ISymbol symbol) visibility = SymbolVisibility.Internal; break; - // For anything else (Public, Protected, ProtectedOrInternal), the - // symbol stays at the level we've gotten so far. + // For anything else (Public, Protected, ProtectedOrInternal), the + // symbol stays at the level we've gotten so far. } current = current.ContainingSymbol; diff --git a/src/CsSig/Analyzer/build/StaticCS.CsSig.props b/src/CsSig/Analyzer/build/StaticCS.CsSig.props index 2604cf4..41404c2 100644 --- a/src/CsSig/Analyzer/build/StaticCS.CsSig.props +++ b/src/CsSig/Analyzer/build/StaticCS.CsSig.props @@ -1,12 +1,13 @@ - - + - - - + + + - diff --git a/test/test/ClosedTests.cs b/test/test/ClosedTests.cs index 529c5aa..1fe1a75 100644 --- a/test/test/ClosedTests.cs +++ b/test/test/ClosedTests.cs @@ -4,10 +4,9 @@ using System.IO; using System.Runtime.CompilerServices; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.CodeAnalysis.CSharp.Testing; using Xunit; namespace StaticCs.Tests; @@ -343,11 +342,11 @@ await VerifyDiagnostics( private static readonly DiagnosticResult ClosedEnumConversion = CSharpAnalyzerVerifier< EnumClosedConversionAnalyzer, - XUnitVerifier + DefaultVerifier >.Diagnostic(DiagId.ClosedEnumConversion.ToIdString()); private static readonly DiagnosticResult ClassOrRecordMustBeClosed = CSharpAnalyzerVerifier< ClosedDeclarationChecker, - XUnitVerifier + DefaultVerifier >.Diagnostic(DiagId.ClassOrRecordMustBeClosed.ToIdString()); private Task VerifyDiagnostics(string src, params DiagnosticResult[] expected) diff --git a/test/test/SuppressorTest.cs b/test/test/SuppressorTest.cs index 28576e3..6da5602 100644 --- a/test/test/SuppressorTest.cs +++ b/test/test/SuppressorTest.cs @@ -5,11 +5,10 @@ using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis.Testing.Verifiers; namespace StaticCs.Tests; -internal class SuppressorTest : CSharpAnalyzerTest +internal class SuppressorTest : CSharpAnalyzerTest where TAnalyzer : DiagnosticAnalyzer, new() { public CSharpCompilationOptions CompilationOptions { get; private init; } = diff --git a/test/test/test.csproj b/test/test/test.csproj index 5cf9705..51f4c22 100644 --- a/test/test/test.csproj +++ b/test/test/test.csproj @@ -11,7 +11,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + From c77bb2a7d8bbae34c2d147ec88d39d32d9ee7658 Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Sun, 28 Jun 2026 11:20:56 -0700 Subject: [PATCH 4/4] Bump StaticCS.CsSig package version to 0.2.0 C# 14 extension-member support is a new feature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/CsSig/CodeFixes/StaticCs.CsSig.CodeFixes.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CsSig/CodeFixes/StaticCs.CsSig.CodeFixes.csproj b/src/CsSig/CodeFixes/StaticCs.CsSig.CodeFixes.csproj index a9b2998..aab98bd 100644 --- a/src/CsSig/CodeFixes/StaticCs.CsSig.CodeFixes.csproj +++ b/src/CsSig/CodeFixes/StaticCs.CsSig.CodeFixes.csproj @@ -14,7 +14,7 @@ --> StaticCS.CsSig - 0.1.0 + 0.2.0 agocke true BSD-3-Clause