Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added PoC for adding OnlyVisibleToType to interfaces. #929

Closed
wants to merge 1 commit into from
Closed
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
9 changes: 9 additions & 0 deletions src/D2L.CodeStyle.Analyzers/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -766,5 +766,14 @@ public static class Diagnostics {
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true
);

public static readonly DiagnosticDescriptor TypeNotVisibleToCaller = new DiagnosticDescriptor(
id: "D2L0103",
title: "Type is not visible to caller",
messageFormat: "The type '{0}' has restricted its visibility to an explicit set of callers",
category: "Correctness",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true
);
}
}
190 changes: 141 additions & 49 deletions src/D2L.CodeStyle.Analyzers/Language/OnlyVisibleToAnalyzer.Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,20 @@ INamedTypeSymbol onlyVisibleToTypeAttribute
}

public static Model? TryCreate( Compilation compilation ) {
INamedTypeSymbol? onlyVisibleToTypeAttribute = compilation
.GetTypeByMetadataName( OnlyVisibleToTypeAttributeMetadataName );

INamedTypeSymbol? onlyVisibleToTypeAttribute = compilation.GetTypeByMetadataName( OnlyVisibleToTypeAttributeMetadataName );
Comment on lines +28 to -29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside (since I'm not looking at the implementation right now), extra non-changes like this make things a lot harder to review.

if( onlyVisibleToTypeAttribute == null ) {
return null;
}

return new Model( compilation, onlyVisibleToTypeAttribute );
}

public bool IsVisibleTo( INamedTypeSymbol caller, ISymbol member ) {
public bool IsVisibleTo( INamedTypeSymbol caller, ISymbol symbol ) {

ImmutableHashSet<INamedTypeSymbol>? restrictions = m_visibilityCache
.GetOrAdd( member, GetTypeVisibilityRestrictions );
.GetOrAdd( symbol, GetSymbolVisibilityRestrictions );

if( restrictions == null ) {
return true;
Expand All @@ -47,88 +48,179 @@ public bool IsVisibleTo( INamedTypeSymbol caller, ISymbol member ) {
return true;
}

if( SymbolEqualityComparer.Default.Equals( caller, member.ContainingType ) ) {
if( SymbolEqualityComparer.Default.Equals( caller, symbol.ContainingType ) ) {
return true;
}

return false;
}

private ImmutableHashSet<INamedTypeSymbol>? GetTypeVisibilityRestrictions( ISymbol member ) {

ImmutableArray<AttributeData> attributes = member.GetAttributes();
if( attributes.IsEmpty ) {
return null;
}
private ImmutableHashSet<INamedTypeSymbol>? GetSymbolVisibilityRestrictions( ISymbol symbol ) {

ImmutableHashSet<INamedTypeSymbol>.Builder? restrictions = null;

foreach( AttributeData attribute in attributes ) {
// Local method to be able to add directly to the builder
void AddTypeRestrictions(
INamedTypeSymbol attributeSymbol,
Compilation compilation,
ISymbol symbol,
bool excludeUninherited
) {
// Get all attributes on the specified symbol
ImmutableArray<AttributeData> attributes = symbol.GetAttributes();
if( attributes.IsEmpty ) {
return;
}

if( SymbolEqualityComparer.Default.Equals( attribute.AttributeClass, m_onlyVisibleToTypeAttribute ) ) {
foreach( AttributeData attribute in attributes ) {
// Ignore any attributes that are not the relevant attribute
if( !SymbolEqualityComparer.Default.Equals( attribute.AttributeClass, attributeSymbol ) ) {
continue;
}

restrictions ??= ImmutableHashSet.CreateBuilder<INamedTypeSymbol>( SymbolEqualityComparer.Default );
restrictions ??= ImmutableHashSet.CreateBuilder<INamedTypeSymbol>(
SymbolEqualityComparer.Default
);

INamedTypeSymbol? visibleToType = TryGetOnlyVisibleToType( attribute );
if( visibleToType != null ) {
// Retrieve the referenced type and inheritance from the attribute
AttributeInfo? attributeInfo = GetAttributeInfo( attribute, compilation );

restrictions.Add( visibleToType );
// Ignore any attributes which could not be retrieved
if( attributeInfo == null ) {
continue;
}

// Conditionally ignore any attributes which are not inherited
// (if `Foo : IBar`, this prevents references of `Foo` from
// throwing an error if `IBar` has restrictions that are not inherited)
if( excludeUninherited && !attributeInfo.Value.IsInherited ) {
continue;
}

restrictions.Add( attributeInfo.Value.Symbol );
}
}

// null implies that we never saw any [OnlyVisibleToType] attributes
// Get any restrictions that are explicitly on the symbol
AddTypeRestrictions(
attributeSymbol: m_onlyVisibleToTypeAttribute,
compilation: m_compilation,
symbol: symbol,
excludeUninherited: false
);

// Only named types have bases, so if the symbol is
// a named type then we need to look at its bases
if( symbol is INamedTypeSymbol namedTypeSymbol ) {

// Check visibility against all base classes
INamedTypeSymbol? baseType = namedTypeSymbol.BaseType;
while( baseType is not null ) {
AddTypeRestrictions(
attributeSymbol: m_onlyVisibleToTypeAttribute,
compilation: m_compilation,
symbol: baseType,
excludeUninherited: true
);
baseType = baseType.BaseType;
}

// Check visibility against all implemented interfaces
foreach( INamedTypeSymbol interfaceSymbol in namedTypeSymbol.AllInterfaces ) {
AddTypeRestrictions(
attributeSymbol: m_onlyVisibleToTypeAttribute,
compilation: m_compilation,
symbol: interfaceSymbol,
excludeUninherited: true
);
}
}

// Null implies that we never saw any [OnlyVisibleToType] attributes
if( restrictions == null ) {
return null;
}

// could be empty in the case where we saw an [OnlyVisibleTo] attribute but none of the indicated types are in this compilation
// Could be empty in the case where we saw an [OnlyVisibleTo] attribute
// but none of the indicated types are in this compilation
return restrictions.ToImmutable();
}

private INamedTypeSymbol? TryGetOnlyVisibleToType( AttributeData attribute ) {

ImmutableArray<TypedConstant> arguments = attribute.ConstructorArguments;

if( arguments.Length == 2 ) {
private static AttributeInfo? GetAttributeInfo(
AttributeData attribute,
Compilation compilation
) {
ImmutableArray<TypedConstant> attributeArgs = attribute.ConstructorArguments;
return attributeArgs.Length switch {
2 => GetFullyTypedAttributeInfo( attributeArgs ),
3 => GetQualifiedAttributeInfo( attributeArgs, compilation ),
_ => null,
};
}

TypedConstant metadataNameArgument = arguments[ 0 ];
if( metadataNameArgument.Value is not string metadataName ) {
return null;
}
private static AttributeInfo? GetFullyTypedAttributeInfo(
ImmutableArray<TypedConstant> arguments
) {
TypedConstant typeArgument = arguments[0];
if( typeArgument.Value is not INamedTypeSymbol type ) {
return null;
}

TypedConstant assemblyNameAttribute = arguments[ 1 ];
if( assemblyNameAttribute.Value is not string assemblyName ) {
return null;
}
TypedConstant inheritedArgument = arguments[1];
if( inheritedArgument.Value is not bool inherited ) {
return null;
}

INamedTypeSymbol? type = m_compilation.GetTypeByMetadataName( metadataName );
if( type == null ) {
return null;
}
if( type.IsUnboundGenericType ) {
return new AttributeInfo {
Symbol = type.OriginalDefinition,
IsInherited = inherited
};
}

if( !type.ContainingAssembly.Name.Equals( assemblyName, StringComparison.Ordinal ) ) {
return null;
}
return new AttributeInfo {
Symbol = type,
IsInherited = inherited
};
}

return type;
private static AttributeInfo? GetQualifiedAttributeInfo(
ImmutableArray<TypedConstant> arguments,
Compilation compilation
) {
TypedConstant metadataNameArgument = arguments[0];
if( metadataNameArgument.Value is not string metadataName ) {
return null;
}

if( arguments.Length == 1 ) {
TypedConstant assemblyNameArgument = arguments[1];
if( assemblyNameArgument.Value is not string assemblyName ) {
return null;
}

TypedConstant typeArgument = arguments[ 0 ];
if( typeArgument.Value is not INamedTypeSymbol type ) {
return null;
}
INamedTypeSymbol? type = compilation.GetTypeByMetadataName( metadataName );
if( type == null ) {
return null;
}

if( type.IsUnboundGenericType ) {
return type.OriginalDefinition;
}
TypedConstant inheritedArgument = arguments[2];
if( inheritedArgument.Value is not bool inherited ) {
return null;
}

return type;
if( !type.ContainingAssembly.Name.Equals( assemblyName, StringComparison.Ordinal ) ) {
return null;
}

return null;
return new AttributeInfo {
Symbol = type,
IsInherited = inherited
};
}

private readonly struct AttributeInfo {
public INamedTypeSymbol Symbol { get; init; }
public bool IsInherited { get; init; }
}
}
}
Expand Down
52 changes: 50 additions & 2 deletions src/D2L.CodeStyle.Analyzers/Language/OnlyVisibleToAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

Expand All @@ -9,7 +11,8 @@ namespace D2L.CodeStyle.Analyzers.Language {
public sealed partial class OnlyVisibleToAnalyzer : DiagnosticAnalyzer {

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
Diagnostics.MemberNotVisibleToCaller
Diagnostics.MemberNotVisibleToCaller,
Diagnostics.TypeNotVisibleToCaller
);

public override void Initialize( AnalysisContext context ) {
Expand Down Expand Up @@ -60,14 +63,18 @@ public static void OnCompilationStart( CompilationStartAnalysisContext context )
},
OperationKind.PropertyReference
);

context.RegisterSyntaxNodeAction(
context => AnalyzeTypeUsage( context, (IdentifierNameSyntax)context.Node, model ),
SyntaxKind.IdentifierName
);
}

private static void AnalyzeMemberUsage(
OperationAnalysisContext context,
ISymbol member,
in Model model
) {

INamedTypeSymbol caller = context.ContainingSymbol.ContainingType;
if( model.IsVisibleTo( caller, member ) ) {
return;
Expand All @@ -83,5 +90,46 @@ in Model model

context.ReportDiagnostic( diagnostic );
}

private static void AnalyzeTypeUsage(
SyntaxNodeAnalysisContext context,
IdentifierNameSyntax node,
in Model model
) {
INamedTypeSymbol? caller = context.ContainingSymbol?.ContainingType;
if( caller == null ) {
return;
}

ISymbol? originalDefinition = context
.SemanticModel
.GetSymbolInfo( node )
.Symbol?
.OriginalDefinition;
if( originalDefinition is not INamedTypeSymbol symbol ) {
return;
}

if( symbol.TypeKind != TypeKind.Interface
&& symbol.TypeKind != TypeKind.Class ) {
return;
}

if( model.IsVisibleTo( caller, symbol ) ) {
return;
}

Diagnostic diagnostic = Diagnostic.Create(
descriptor: Diagnostics.TypeNotVisibleToCaller,
location: node.Parent is QualifiedNameSyntax qns
? qns.GetLocation()
: node.GetLocation(),
messageArgs: new[] {
symbol.Name
}
);

context.ReportDiagnostic( diagnostic );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace D2L.CodeStyle.Annotations.Contract {

[AttributeUsage(
validOn: AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property,
validOn: AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Property,
AllowMultiple = true,
Inherited = false
)]
Expand All @@ -13,21 +13,32 @@ public sealed class OnlyVisibleToTypeAttribute : Attribute {
/// Restricts the visibility to the specified type.
/// </summary>
/// <param name="type">The non-generic type or the generic type definition (e.g. System.Span&lt;&gt;) to be made visible to.</param>
public OnlyVisibleToTypeAttribute( Type type )
: this( type.FullName, type.Assembly.GetName().Name ) {
/// <param name="inherited">Whether the restriction should also apply to derived types.</param>
public OnlyVisibleToTypeAttribute(
Type type,
bool inherited = true
)
: this( type.FullName, type.Assembly.GetName().Name, inherited ) {
}

/// <summary>
/// Restricts the visibility to the specified type.
/// </summary>
/// <param name="fullyQualifiedTypeName">The non-generic type or the generic type definition (e.g. System.Span`1) to be made visible to.</param>
/// <param name="assemblyName">The name of the assembly containing the type.</param>
public OnlyVisibleToTypeAttribute( string fullyQualifiedTypeName, string assemblyName ) {
/// <param name="inherited">Whether the restriction should also apply to derived types.</param>
public OnlyVisibleToTypeAttribute(
string fullyQualifiedTypeName,
string assemblyName,
bool inherited = true
) {
FullyQualifiedTypeName = fullyQualifiedTypeName;
AssemblyName = assemblyName;
Inherited = inherited;
}

public string FullyQualifiedTypeName { get; }
public string AssemblyName { get; }
public bool Inherited { get; }
}
}
Loading
Loading