|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
#nullable enable
namespace Microsoft.AspNetCore.Components.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class AuthenticationStateProviderAnalyzer : DiagnosticAnalyzer
{
public AuthenticationStateProviderAnalyzer()
{
SupportedDiagnostics = ImmutableArray.Create(
DiagnosticDescriptors.AuthenticationStateProviderCachedWithoutSubscription);
}
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.RegisterCompilationStartAction(context =>
{
var authStateProviderType = context.Compilation.GetTypeByMetadataName(ComponentsApi.AuthenticationStateProvider.MetadataName);
if (authStateProviderType is null)
{
return;
}
context.RegisterSymbolStartAction(context =>
{
var namedType = (INamedTypeSymbol)context.Symbol;
// Only analyze types that have access to an AuthenticationStateProvider
// (either through a field/property or by inheriting from it)
if (!TypeUsesAuthenticationStateProvider(namedType, authStateProviderType))
{
return;
}
var hasGetAuthStateCall = false;
var hasAuthStateChangedSubscription = false;
context.RegisterOperationAction(operationContext =>
{
var invocation = (IInvocationOperation)operationContext.Operation;
if (invocation.Instance is not null &&
IsAuthenticationStateProviderType(invocation.Instance.Type, authStateProviderType) &&
invocation.TargetMethod.Name == ComponentsApi.AuthenticationStateProvider.GetAuthenticationStateAsync)
{
hasGetAuthStateCall = true;
}
}, OperationKind.Invocation);
context.RegisterOperationAction(operationContext =>
{
var eventAssignment = (IEventAssignmentOperation)operationContext.Operation;
if (eventAssignment.Adds &&
eventAssignment.EventReference is IEventReferenceOperation eventRef &&
IsAuthenticationStateProviderType(eventRef.Instance?.Type, authStateProviderType) &&
eventRef.Event.Name == ComponentsApi.AuthenticationStateProvider.AuthenticationStateChanged)
{
hasAuthStateChangedSubscription = true;
}
}, OperationKind.EventAssignment);
context.RegisterSymbolEndAction(endContext =>
{
if (hasGetAuthStateCall && !hasAuthStateChangedSubscription)
{
endContext.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.AuthenticationStateProviderCachedWithoutSubscription,
namedType.Locations.FirstOrDefault(),
namedType.Name));
}
});
}, SymbolKind.NamedType);
});
}
private static bool TypeUsesAuthenticationStateProvider(INamedTypeSymbol type, INamedTypeSymbol authStateProviderType)
{
// Check if the type itself derives from AuthenticationStateProvider
if (IsAuthenticationStateProviderType(type, authStateProviderType))
{
return true;
}
// Check if any field or property (including inheritance chain) is of type AuthenticationStateProvider
for (var currentType = type; currentType is not null; currentType = currentType.BaseType)
{
foreach (var member in currentType.GetMembers())
{
if (member is IPropertySymbol property && IsAuthenticationStateProviderType(property.Type, authStateProviderType))
{
// The property could be private, but there can be another non-private property further down
// the inheritance chain that exposes the same type, so don't back out of the loop.
if (!SymbolEqualityComparer.Default.Equals(currentType, type) && property.DeclaredAccessibility == Accessibility.Private)
{
continue;
}
// Only consider properties that are not private in base types because they cannot be accessed by derived types.
return SymbolEqualityComparer.Default.Equals(currentType, type) || property.DeclaredAccessibility != Accessibility.Private;
}
if (member is IFieldSymbol field && IsAuthenticationStateProviderType(field.Type, authStateProviderType))
{
// The field could be backed by a property and even if the field is private, the property may not, so don't back out of the loop.
if (!SymbolEqualityComparer.Default.Equals(currentType, type) && field.DeclaredAccessibility == Accessibility.Private)
{
continue;
}
// Only consider fields that are not private in base types because they cannot be accessed by derived types.
return SymbolEqualityComparer.Default.Equals(currentType, type) || field.DeclaredAccessibility != Accessibility.Private;
}
}
}
return false;
}
private static bool IsAuthenticationStateProviderType(ITypeSymbol? type, INamedTypeSymbol authStateProviderType)
{
if (type is null)
{
return false;
}
// Check if the type is AuthenticationStateProvider or derives from it
var current = type;
while (current is not null)
{
if (SymbolEqualityComparer.Default.Equals(current, authStateProviderType))
{
return true;
}
current = current.BaseType;
}
return false;
}
}
|