From 0be14d7ac17dafafb67cbe0b6ab435e243b6523d Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Mon, 10 Aug 2026 14:57:30 +0200 Subject: [PATCH 1/2] Ensure that internal code is never exposed through InternalsVisibleTo Reflectify is a source-only package, so its types are compiled directly into the consuming assembly. Until now those types were plain internal types, which means any assembly listed in the consumer's InternalsVisibleTo could see and use them. That makes Reflectify part of the consumer's internal API surface without the consumer asking for it. Mark every type with [Microsoft.CodeAnalysis.Embedded] when compiled into a consumer. Roslyn hides embedded types from all other assemblies, including friend assemblies. Inside Reflectify's own build the types stay public instead, which lets the spec project reference them normally and allowed the InternalsVisibleTo entry to be dropped. This ports dennisdoomen/pathy#54. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Reflectify/EmbeddedAttribute.cs | 29 +++++++++++++++++++++++ src/Reflectify/MemberInfoExtensions.cs | 6 +++++ src/Reflectify/MemberKind.cs | 11 +++++++++ src/Reflectify/ParameterInfoExtensions.cs | 6 +++++ src/Reflectify/PropertyInfoExtensions.cs | 6 +++++ src/Reflectify/Reflectify.csproj | 7 +----- src/Reflectify/Reflector.cs | 8 ++++++- src/Reflectify/TypeExtensions.cs | 6 +++++ src/Reflectify/TypeMemberExtensions.cs | 6 +++++ src/Reflectify/TypeMetaDataExtensions.cs | 6 +++++ 10 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 src/Reflectify/EmbeddedAttribute.cs diff --git a/src/Reflectify/EmbeddedAttribute.cs b/src/Reflectify/EmbeddedAttribute.cs new file mode 100644 index 0000000..cfb2c14 --- /dev/null +++ b/src/Reflectify/EmbeddedAttribute.cs @@ -0,0 +1,29 @@ +#if !REFLECTIFY_COMPILE +// +#pragma warning disable +#endif + +#nullable disable + +// Another source-only package compiled into the same assembly may already declare this attribute. Define +// REFLECTIFY_EXCLUDE_EMBEDDED_ATTRIBUTE to suppress this declaration and use that one instead. +#if !REFLECTIFY_EXCLUDE_EMBEDDED_ATTRIBUTE + +using System; +using System.Diagnostics.CodeAnalysis; + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +namespace Microsoft.CodeAnalysis; + +/// +/// A special attribute recognized by Roslyn, that marks a type as "embedded", meaning it won't ever be visible from +/// other assemblies. +/// +[AttributeUsage(AttributeTargets.All)] +[ExcludeFromCodeCoverage] +internal sealed class EmbeddedAttribute : Attribute +{ +} + +#endif diff --git a/src/Reflectify/MemberInfoExtensions.cs b/src/Reflectify/MemberInfoExtensions.cs index 36597b0..2f6be08 100644 --- a/src/Reflectify/MemberInfoExtensions.cs +++ b/src/Reflectify/MemberInfoExtensions.cs @@ -11,7 +11,13 @@ namespace Reflectify; +#if REFLECTIFY_COMPILE +public static class MemberInfoExtensions +#else +[global::Microsoft.CodeAnalysis.Embedded] +[global::System.Diagnostics.DebuggerNonUserCode] internal static class MemberInfoExtensions +#endif { public static bool HasAttribute(this MemberInfo member) where TAttribute : Attribute diff --git a/src/Reflectify/MemberKind.cs b/src/Reflectify/MemberKind.cs index 0580cf8..3c211c7 100644 --- a/src/Reflectify/MemberKind.cs +++ b/src/Reflectify/MemberKind.cs @@ -14,7 +14,12 @@ namespace Reflectify; /// Defines the kinds of members you want to get when querying for the fields and properties of a type. /// [Flags] +#if REFLECTIFY_COMPILE +public enum MemberKind +#else +[global::Microsoft.CodeAnalysis.Embedded] internal enum MemberKind +#endif { None, Public = 1, @@ -24,7 +29,13 @@ internal enum MemberKind Static = 16 } +#if REFLECTIFY_COMPILE +public static class MemberKindExtensions +#else +[global::Microsoft.CodeAnalysis.Embedded] +[global::System.Diagnostics.DebuggerNonUserCode] internal static class MemberKindExtensions +#endif { public static BindingFlags ToBindingFlags(this MemberKind kind) { diff --git a/src/Reflectify/ParameterInfoExtensions.cs b/src/Reflectify/ParameterInfoExtensions.cs index 9e57ddb..3dd82aa 100644 --- a/src/Reflectify/ParameterInfoExtensions.cs +++ b/src/Reflectify/ParameterInfoExtensions.cs @@ -11,7 +11,13 @@ namespace Reflectify; +#if REFLECTIFY_COMPILE +public static class ParameterInfoExtensions +#else +[global::Microsoft.CodeAnalysis.Embedded] +[global::System.Diagnostics.DebuggerNonUserCode] internal static class ParameterInfoExtensions +#endif { /// /// Returns if the parameter is decorated with the specific , diff --git a/src/Reflectify/PropertyInfoExtensions.cs b/src/Reflectify/PropertyInfoExtensions.cs index c5e6509..e049f95 100644 --- a/src/Reflectify/PropertyInfoExtensions.cs +++ b/src/Reflectify/PropertyInfoExtensions.cs @@ -9,7 +9,13 @@ namespace Reflectify; +#if REFLECTIFY_COMPILE +public static class PropertyInfoExtensions +#else +[global::Microsoft.CodeAnalysis.Embedded] +[global::System.Diagnostics.DebuggerNonUserCode] internal static class PropertyInfoExtensions +#endif { /// /// Returns if the property is an indexer, or otherwise. diff --git a/src/Reflectify/Reflectify.csproj b/src/Reflectify/Reflectify.csproj index f5a3e35..1a674e5 100644 --- a/src/Reflectify/Reflectify.csproj +++ b/src/Reflectify/Reflectify.csproj @@ -8,14 +8,9 @@ .nuspec version=$(Version) REFLECTIFY_COMPILE + 1591;1573 - - - <_Parameter1>Reflectify.Specs - - - bin\Debug\Reflectify.xml diff --git a/src/Reflectify/Reflector.cs b/src/Reflectify/Reflector.cs index 96df0d2..cb2ad2b 100644 --- a/src/Reflectify/Reflector.cs +++ b/src/Reflectify/Reflector.cs @@ -15,7 +15,13 @@ namespace Reflectify; /// /// Helper class to get all the public and internal fields and properties from a type. /// +#if REFLECTIFY_COMPILE +public sealed class Reflector(Type typeToReflect, MemberKind kind) +#else +[global::Microsoft.CodeAnalysis.Embedded] +[global::System.Diagnostics.DebuggerNonUserCode] internal sealed class Reflector(Type typeToReflect, MemberKind kind) +#endif { private readonly object lazyLoadingLock = new(); private volatile PropertyInfo[] cachedProperties; @@ -173,7 +179,7 @@ private static bool HasVisibility(MemberKind kind, FieldInfo field) ((kind & MemberKind.Internal) != MemberKind.None && (field.IsAssembly || field.IsFamilyOrAssembly)); } - private class OrderedPropertyCollection + private sealed class OrderedPropertyCollection { private readonly Dictionary kindMap = new(); private readonly List<(string Name, PropertyInfo Property)> propertiesWithName = new(); diff --git a/src/Reflectify/TypeExtensions.cs b/src/Reflectify/TypeExtensions.cs index ddfc792..d18d316 100644 --- a/src/Reflectify/TypeExtensions.cs +++ b/src/Reflectify/TypeExtensions.cs @@ -9,7 +9,13 @@ namespace Reflectify; +#if REFLECTIFY_COMPILE +public static class TypeExtensions +#else +[global::Microsoft.CodeAnalysis.Embedded] +[global::System.Diagnostics.DebuggerNonUserCode] internal static class TypeExtensions +#endif { /// /// If the type provided is a nullable type, gets the underlying type. Returns the type itself otherwise. diff --git a/src/Reflectify/TypeMemberExtensions.cs b/src/Reflectify/TypeMemberExtensions.cs index 86b98a9..af8941f 100644 --- a/src/Reflectify/TypeMemberExtensions.cs +++ b/src/Reflectify/TypeMemberExtensions.cs @@ -13,7 +13,13 @@ namespace Reflectify; +#if REFLECTIFY_COMPILE +public static class TypeMemberExtensions +#else +[global::Microsoft.CodeAnalysis.Embedded] +[global::System.Diagnostics.DebuggerNonUserCode] internal static class TypeMemberExtensions +#endif { private static readonly ConcurrentDictionary<(Type Type, MemberKind Kind), Reflector> ReflectorCache = new(); diff --git a/src/Reflectify/TypeMetaDataExtensions.cs b/src/Reflectify/TypeMetaDataExtensions.cs index 4c5d4f2..d4062e1 100644 --- a/src/Reflectify/TypeMetaDataExtensions.cs +++ b/src/Reflectify/TypeMetaDataExtensions.cs @@ -14,7 +14,13 @@ namespace Reflectify; +#if REFLECTIFY_COMPILE +public static class TypeMetaDataExtensions +#else +[global::Microsoft.CodeAnalysis.Embedded] +[global::System.Diagnostics.DebuggerNonUserCode] internal static class TypeMetaDataExtensions +#endif { /// /// Returns the name of the type without the generic backtick and the type arguments. From 8e46954f8c23a321a6700390aeb446f145d3d0eb Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Mon, 10 Aug 2026 16:26:15 +0200 Subject: [PATCH 2/2] Let a duplicate EmbeddedAttribute merge instead of collide Pathy is also a source-only package and declares the same Microsoft.CodeAnalysis.EmbeddedAttribute. A consumer using both compiles two copies into one assembly, which is a duplicate type error. Declaring the attribute as partial makes the two declarations merge into a single type. This needs no cooperation from the consumer, so the REFLECTIFY_EXCLUDE_EMBEDDED_ATTRIBUTE opt-out is no longer needed. Partial types cannot repeat a type-level attribute across parts, so [AttributeUsage] and [ExcludeFromCodeCoverage] had to go. Dropping [AttributeUsage] changes nothing, because AttributeTargets.All is already the default. Three analyzers ask for it back, so suppress them here. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Reflectify/EmbeddedAttribute.cs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Reflectify/EmbeddedAttribute.cs b/src/Reflectify/EmbeddedAttribute.cs index cfb2c14..9fb5f2a 100644 --- a/src/Reflectify/EmbeddedAttribute.cs +++ b/src/Reflectify/EmbeddedAttribute.cs @@ -5,13 +5,6 @@ #nullable disable -// Another source-only package compiled into the same assembly may already declare this attribute. Define -// REFLECTIFY_EXCLUDE_EMBEDDED_ATTRIBUTE to suppress this declaration and use that one instead. -#if !REFLECTIFY_EXCLUDE_EMBEDDED_ATTRIBUTE - -using System; -using System.Diagnostics.CodeAnalysis; - // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.CodeAnalysis; @@ -20,10 +13,14 @@ namespace Microsoft.CodeAnalysis; /// A special attribute recognized by Roslyn, that marks a type as "embedded", meaning it won't ever be visible from /// other assemblies. /// -[AttributeUsage(AttributeTargets.All)] -[ExcludeFromCodeCoverage] -internal sealed class EmbeddedAttribute : Attribute +/// +/// Another source-only package compiled into the same assembly may declare this attribute too. Declaring it as +/// partial lets both declarations merge into one type instead of colliding. For that to work, no part may +/// carry a type-level attribute such as [AttributeUsage], because attributes cannot be repeated across parts. +/// Leaving it off is harmless: AttributeTargets.All is the default. +/// +#pragma warning disable RCS1203, MA0010, CA1018 // Deliberately omitted so that duplicate declarations can merge, see above. +internal sealed partial class EmbeddedAttribute : System.Attribute +#pragma warning restore RCS1203, MA0010, CA1018 { } - -#endif