Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/CsSig/Analyzer/AnalyzerRules.globalconfig
Original file line number Diff line number Diff line change
@@ -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
143 changes: 89 additions & 54 deletions src/CsSig/Analyzer/ApiSurface.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
Expand All @@ -21,26 +22,23 @@ internal static class ApiSurface
/// A readable signature format, used only for diagnostic messages. Equivalence is decided by
/// the structural <see cref="ApiMember"/>, not by this string.
/// </summary>
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
);

/// <summary>
/// Builds a map from the structural signature of every externally visible member declared in
Expand All @@ -57,26 +55,23 @@ public static Dictionary<MemberIdentity, ApiEntry> 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<MemberIdentity, ApiEntry> 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)
Expand All @@ -92,7 +87,7 @@ private static Location GetLocation(ISymbol symbol)

/// <summary>Yields the tracked members of <paramref name="type"/>, including implicit
/// constructors and implicit record members, mirroring the Public API analyzer.</summary>
private static IEnumerable<ISymbol> GetApiMembers(INamedTypeSymbol type)
private static void AddApiMembers(INamedTypeSymbol type, Action<ISymbol> add)
{
foreach (var member in type.GetMembers())
{
Expand All @@ -109,46 +104,69 @@ private static IEnumerable<ISymbol> 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);
}
}

// 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;
}

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<INamedTypeSymbol> AllNamedTypes(INamespaceSymbol root)
private static List<INamedTypeSymbol> AllNamedTypes(INamespaceSymbol root)
{
var result = new List<INamedTypeSymbol>();
var stack = new Stack<INamespaceOrTypeSymbol>();
stack.Push(root);

Expand All @@ -163,12 +181,14 @@ private static IEnumerable<INamedTypeSymbol> AllNamedTypes(INamespaceSymbol root
stack.Push(ns);
break;
case INamedTypeSymbol type:
yield return type;
result.Add(type);
stack.Push(type);
break;
}
}
}

return result;
}

/// <summary>
Expand All @@ -186,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;
}
Expand All @@ -217,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;
}
Expand All @@ -233,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,
}
);
}

/// <summary>
Expand Down
Loading
Loading