Skip to content

Commit 2fa3961

Browse files
authored
Merge pull request #89 from CommunityToolkit/dev/nullability-attributes-check
Fix conflicting generation of nullability attributes
2 parents af106bf + 7d52b58 commit 2fa3961

File tree

6 files changed

+141
-7
lines changed

6 files changed

+141
-7
lines changed

CommunityToolkit.Mvvm.SourceGenerators/Attributes/NullabilityAttributesGenerator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Linq;
77
using System.Reflection;
88
using System.Text;
9+
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
910
using Microsoft.CodeAnalysis;
1011
using Microsoft.CodeAnalysis.Text;
1112

@@ -38,7 +39,7 @@ private void AddSourceCodeIfTypeIsNotPresent(GeneratorExecutionContext context,
3839
// this works fine both in .NET (Core) and .NET Standard implementations, we also need to check
3940
// that the target types are declared as public (we assume that in this case those types are from the BCL).
4041
// This avoids issues on .NET Standard with Roslyn also seeing internal types from referenced assemblies.
41-
if (context.Compilation.GetTypeByMetadataName(typeFullName) is { DeclaredAccessibility: Accessibility.Public })
42+
if (context.Compilation.HasAccessibleTypeWithMetadataName(typeFullName))
4243
{
4344
return;
4445
}

CommunityToolkit.Mvvm.SourceGenerators/EmbeddedResources/NotNullAttribute.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ namespace System.Diagnostics.CodeAnalysis
66
{
77
/// <summary>
88
/// Specifies that an output will not be null even if the corresponding type allows it.
9-
/// Specifies that an input argument was not null when the call returns.
9+
/// Also specifies that an input argument was not null when the call returns.
1010
/// </summary>
11-
/// <remarks>Internal copy from the BCL attribute.</remarks>
1211
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
1312
internal sealed class NotNullAttribute : Attribute
1413
{

CommunityToolkit.Mvvm.SourceGenerators/EmbeddedResources/NotNullIfNotNullAttribute.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,21 @@ namespace System.Diagnostics.CodeAnalysis
77
/// <summary>
88
/// Specifies that the output will be non-null if the named parameter is non-null.
99
/// </summary>
10-
/// <remarks>Internal copy from the BCL attribute.</remarks>
1110
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)]
1211
internal sealed class NotNullIfNotNullAttribute : Attribute
1312
{
1413
/// <summary>
1514
/// Initializes a new instance of the <see cref="NotNullIfNotNullAttribute"/> class.
1615
/// </summary>
1716
/// <param name="parameterName"> The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.</param>
18-
public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName;
17+
public NotNullIfNotNullAttribute(string parameterName)
18+
{
19+
ParameterName = parameterName;
20+
}
1921

20-
/// <summary>Gets the associated parameter name.</summary>
22+
/// <summary>
23+
/// Gets the associated parameter name.
24+
/// </summary>
2125
public string ParameterName { get; }
2226
}
2327
}

CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
// more info in ThirdPartyNotices.txt in the root of the project.
77

88
using System.Collections.Generic;
9-
using System.Diagnostics.Contracts;
109
using Microsoft.CodeAnalysis;
1110

1211
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis;
6+
7+
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
8+
9+
/// <summary>
10+
/// Extension methods for the <see cref="Compilation"/> type.
11+
/// </summary>
12+
internal static class CompilationExtensions
13+
{
14+
/// <summary>
15+
/// <para>
16+
/// Checks whether or not a type with a specified metadata name is accessible from a given <see cref="Compilation"/> instance.
17+
/// </para>
18+
/// <para>
19+
/// This method enumerates candidate type symbols to find a match in the following order:
20+
/// <list type="number">
21+
/// <item><description>
22+
/// If only one type with the given name is found within the compilation and its referenced assemblies, check its accessibility.
23+
/// </description></item>
24+
/// <item><description>
25+
/// If the current <paramref name="compilation"/> defines the symbol, check its accessibility.
26+
/// </description></item>
27+
/// <item><description>
28+
/// Otherwise, check whether the type exists and is accessible from any of the referenced assemblies.
29+
/// </description></item>
30+
/// </list>
31+
/// </para>
32+
/// </summary>
33+
/// <param name="compilation">The <see cref="Compilation"/> to consider for analysis.</param>
34+
/// <param name="fullyQualifiedMetadataName">The fully-qualified metadata type name to find.</param>
35+
/// <returns>Whether a type with the specified metadata name can be accessed from the given compilation.</returns>
36+
public static bool HasAccessibleTypeWithMetadataName(this Compilation compilation, string fullyQualifiedMetadataName)
37+
{
38+
// Try to get the unique type with this name
39+
INamedTypeSymbol? type = compilation.GetTypeByMetadataName(fullyQualifiedMetadataName);
40+
41+
// If there is only a single matching symbol, check its accessibility
42+
if (type is not null)
43+
{
44+
return type.CanBeAccessedFrom(compilation.Assembly);
45+
}
46+
47+
// Otherwise, try to get the unique type with this name originally defined in 'compilation'
48+
type ??= compilation.Assembly.GetTypeByMetadataName(fullyQualifiedMetadataName);
49+
50+
if (type is not null)
51+
{
52+
return type.CanBeAccessedFrom(compilation.Assembly);
53+
}
54+
55+
// Otherwise, check whether the type is defined and accessible from any of the referenced assemblies
56+
foreach (IModuleSymbol module in compilation.Assembly.Modules)
57+
{
58+
foreach (IAssemblySymbol referencedAssembly in module.ReferencedAssemblySymbols)
59+
{
60+
if (referencedAssembly.GetTypeByMetadataName(fullyQualifiedMetadataName) is not INamedTypeSymbol currentType)
61+
{
62+
continue;
63+
}
64+
65+
switch (currentType.GetEffectiveAccessibility())
66+
{
67+
case Accessibility.Public:
68+
case Accessibility.Internal when referencedAssembly.GivesAccessTo(compilation.Assembly):
69+
return true;
70+
default:
71+
continue;
72+
}
73+
}
74+
}
75+
76+
return false;
77+
}
78+
}

CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,57 @@ public static bool HasFullyQualifiedName(this ISymbol symbol, string name)
3131
{
3232
return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == name;
3333
}
34+
35+
/// <summary>
36+
/// Calculates the effective accessibility for a given symbol.
37+
/// </summary>
38+
/// <param name="symbol">The <see cref="ISymbol"/> instance to check.</param>
39+
/// <returns>The effective accessibility for <paramref name="symbol"/>.</returns>
40+
public static Accessibility GetEffectiveAccessibility(this ISymbol symbol)
41+
{
42+
// Start by assuming it's visible
43+
Accessibility visibility = Accessibility.Public;
44+
45+
// Handle special cases
46+
switch (symbol.Kind)
47+
{
48+
case SymbolKind.Alias: return Accessibility.Private;
49+
case SymbolKind.Parameter: return GetEffectiveAccessibility(symbol.ContainingSymbol);
50+
case SymbolKind.TypeParameter: return Accessibility.Private;
51+
}
52+
53+
// Traverse the symbol hierarchy to determine the effective accessibility
54+
while (symbol is not null && symbol.Kind != SymbolKind.Namespace)
55+
{
56+
switch (symbol.DeclaredAccessibility)
57+
{
58+
case Accessibility.NotApplicable:
59+
case Accessibility.Private:
60+
return Accessibility.Private;
61+
case Accessibility.Internal:
62+
case Accessibility.ProtectedAndInternal:
63+
visibility = Accessibility.Internal;
64+
break;
65+
}
66+
67+
symbol = symbol.ContainingSymbol;
68+
}
69+
70+
return visibility;
71+
}
72+
73+
/// <summary>
74+
/// Checks whether or not a given symbol can be accessed from a specified assembly.
75+
/// </summary>
76+
/// <param name="symbol">The input <see cref="ISymbol"/> instance to check.</param>
77+
/// <param name="assembly">The assembly to check the accessibility of <paramref name="symbol"/> for.</param>
78+
/// <returns>Whether <paramref name="assembly"/> can access <paramref name="symbol"/>.</returns>
79+
public static bool CanBeAccessedFrom(this ISymbol symbol, IAssemblySymbol assembly)
80+
{
81+
Accessibility accessibility = symbol.GetEffectiveAccessibility();
82+
83+
return
84+
accessibility == Accessibility.Public ||
85+
accessibility == Accessibility.Internal && symbol.ContainingAssembly.GivesAccessTo(assembly);
86+
}
3487
}

0 commit comments

Comments
 (0)