|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.AspNetCore.Shared;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Microsoft.AspNetCore.Mvc.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class TopLevelParameterNameAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
DiagnosticDescriptors.MVC1004_ParameterNameCollidesWithTopLevelProperty);
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterCompilationStartAction(context =>
{
if (!SymbolCache.TryCreate(context.Compilation, out var typeCache))
{
// No-op if we can't find types we care about.
return;
}
InitializeWorker(context, typeCache);
});
}
private static void InitializeWorker(CompilationStartAnalysisContext context, SymbolCache symbolCache)
{
context.RegisterSymbolAction(context =>
{
var method = (IMethodSymbol)context.Symbol;
if (method.MethodKind != MethodKind.Ordinary)
{
return;
}
if (method.Parameters.Length == 0)
{
return;
}
if (!MvcFacts.IsController(method.ContainingType, symbolCache.ControllerAttribute, symbolCache.NonControllerAttribute) ||
!MvcFacts.IsControllerAction(method, symbolCache.NonActionAttribute, symbolCache.IDisposableDispose))
{
return;
}
if (method.ContainingType.HasAttribute(symbolCache.IApiBehaviorMetadata, inherit: true))
{
// The issue of parameter name collision with properties affects complex model-bound types
// and not input formatting. Ignore ApiController instances since they default to formatting.
return;
}
for (var i = 0; i < method.Parameters.Length; i++)
{
var parameter = method.Parameters[i];
if (IsProblematicParameter(symbolCache, parameter))
{
var location = parameter.Locations.Length != 0 ?
parameter.Locations[0] :
Location.None;
context.ReportDiagnostic(
Diagnostic.Create(
DiagnosticDescriptors.MVC1004_ParameterNameCollidesWithTopLevelProperty,
location,
parameter.Type.Name,
parameter.Name));
}
}
}, SymbolKind.Method);
}
internal static bool IsProblematicParameter(in SymbolCache symbolCache, IParameterSymbol parameter)
{
if (parameter.GetAttributes(symbolCache.FromBodyAttribute).Any())
{
// Ignore input formatted parameters.
return false;
}
if (SpecifiesModelType(in symbolCache, parameter))
{
// Ignore parameters that specify a model type.
return false;
}
if (!IsComplexType(parameter.Type))
{
return false;
}
var parameterName = GetName(symbolCache, parameter);
var type = parameter.Type;
while (type != null)
{
foreach (var member in type.GetMembers())
{
if (member.DeclaredAccessibility != Accessibility.Public ||
member.IsStatic ||
member.Kind != SymbolKind.Property)
{
continue;
}
var propertyName = GetName(symbolCache, member);
if (string.Equals(parameterName, propertyName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
type = type.BaseType;
}
return false;
}
private static bool IsComplexType(ITypeSymbol type)
{
// This analyzer should not apply to simple types. In MVC, a simple type is any type that has a type converter that returns true for TypeConverter.CanConvertFrom(typeof(string)).
// Unfortunately there isn't a Roslyn way of determining if a TypeConverter exists for a given symbol or if the converter allows string conversions.
// https://github.com/dotnet/corefx/blob/v3.0.0-preview8.19405.3/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/ReflectTypeDescriptionProvider.cs#L103-L141
// provides a list of types that have built-in converters.
// We'll use a simpler heuristic in the analyzer: A type is simple if it's a value type or if it's in the "System.*" namespace hierarchy.
var @namespace = type.ContainingNamespace?.ToString();
if (@namespace != null)
{
// Things in the System.* namespace hierarchy don't count as complex types. This workarounds
// the problem of discovering type converters on types in mscorlib.
return @namespace != "System" &&
!@namespace.StartsWith("System.", StringComparison.Ordinal);
}
return true;
}
internal static string GetName(in SymbolCache symbolCache, ISymbol symbol)
{
foreach (var attribute in symbol.GetAttributes(symbolCache.IModelNameProvider))
{
// BindAttribute uses the Prefix property as an alias for IModelNameProvider.Name
var nameProperty = SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, symbolCache.BindAttribute) ? "Prefix" : "Name";
// All of the built-in attributes (FromQueryAttribute, ModelBinderAttribute etc) only support setting the name via
// a property. We'll ignore constructor values.
for (var i = 0; i < attribute.NamedArguments.Length; i++)
{
var namedArgument = attribute.NamedArguments[i];
var namedArgumentValue = namedArgument.Value;
if (string.Equals(namedArgument.Key, nameProperty, StringComparison.Ordinal) &&
namedArgumentValue.Kind == TypedConstantKind.Primitive &&
namedArgumentValue.Type.SpecialType == SpecialType.System_String &&
namedArgumentValue.Value is string name)
{
return name;
}
}
}
return symbol.Name;
}
internal static bool SpecifiesModelType(in SymbolCache symbolCache, IParameterSymbol parameterSymbol)
{
foreach (var attribute in parameterSymbol.GetAttributes(symbolCache.IBinderTypeProviderMetadata))
{
// Look for a attribute property named BinderType being assigned. This would match
// [ModelBinder(BinderType = typeof(SomeBinder))]
for (var i = 0; i < attribute.NamedArguments.Length; i++)
{
var namedArgument = attribute.NamedArguments[i];
var namedArgumentValue = namedArgument.Value;
if (string.Equals(namedArgument.Key, "BinderType", StringComparison.Ordinal) &&
namedArgumentValue.Kind == TypedConstantKind.Type)
{
return true;
}
}
// Look for the binder type being specified in the constructor. This would match
// [ModelBinder(typeof(SomeBinder))]
var constructorParameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
for (var i = 0; i < constructorParameters.Length; i++)
{
if (string.Equals(constructorParameters[i].Name, "binderType", StringComparison.Ordinal))
{
// A constructor that requires binderType was used.
return true;
}
}
}
return false;
}
internal readonly struct SymbolCache
{
public SymbolCache(
INamedTypeSymbol bindAttribute,
INamedTypeSymbol controllerAttribute,
INamedTypeSymbol fromBodyAttribute,
INamedTypeSymbol apiBehaviorMetadata,
INamedTypeSymbol binderTypeProviderMetadata,
INamedTypeSymbol modelNameProvider,
INamedTypeSymbol nonControllerAttribute,
INamedTypeSymbol nonActionAttribute,
IMethodSymbol disposableDispose)
{
BindAttribute = bindAttribute;
ControllerAttribute = controllerAttribute;
FromBodyAttribute = fromBodyAttribute;
IApiBehaviorMetadata = apiBehaviorMetadata;
IBinderTypeProviderMetadata = binderTypeProviderMetadata;
IModelNameProvider = modelNameProvider;
NonControllerAttribute = nonControllerAttribute;
NonActionAttribute = nonActionAttribute;
IDisposableDispose = disposableDispose;
}
public static bool TryCreate(Compilation compilation, out SymbolCache symbolCache)
{
symbolCache = default;
if (!TryGetType(SymbolNames.BindAttribute, out var bindAttribute))
{
return false;
}
if (!TryGetType(SymbolNames.ControllerAttribute, out var controllerAttribute))
{
return false;
}
if (!TryGetType(SymbolNames.FromBodyAttribute, out var fromBodyAttribute))
{
return false;
}
if (!TryGetType(SymbolNames.IApiBehaviorMetadata, out var apiBehaviorMetadata))
{
return false;
}
if (!TryGetType(SymbolNames.IBinderTypeProviderMetadata, out var iBinderTypeProviderMetadata))
{
return false;
}
if (!TryGetType(SymbolNames.IModelNameProvider, out var iModelNameProvider))
{
return false;
}
if (!TryGetType(SymbolNames.NonControllerAttribute, out var nonControllerAttribute))
{
return false;
}
if (!TryGetType(SymbolNames.NonActionAttribute, out var nonActionAttribute))
{
return false;
}
var disposable = compilation.GetSpecialType(SpecialType.System_IDisposable);
var members = disposable?.GetMembers(nameof(IDisposable.Dispose));
var idisposableDispose = (IMethodSymbol?)members?[0];
if (idisposableDispose == null)
{
return false;
}
symbolCache = new SymbolCache(
bindAttribute,
controllerAttribute,
fromBodyAttribute,
apiBehaviorMetadata,
iBinderTypeProviderMetadata,
iModelNameProvider,
nonControllerAttribute,
nonActionAttribute,
idisposableDispose);
return true;
bool TryGetType(string typeName, out INamedTypeSymbol typeSymbol)
{
typeSymbol = compilation.GetTypeByMetadataName(typeName);
return typeSymbol != null && typeSymbol.TypeKind != TypeKind.Error;
}
}
public INamedTypeSymbol BindAttribute { get; }
public INamedTypeSymbol ControllerAttribute { get; }
public INamedTypeSymbol FromBodyAttribute { get; }
public INamedTypeSymbol IApiBehaviorMetadata { get; }
public INamedTypeSymbol IBinderTypeProviderMetadata { get; }
public INamedTypeSymbol IModelNameProvider { get; }
public INamedTypeSymbol NonControllerAttribute { get; }
public INamedTypeSymbol NonActionAttribute { get; }
public IMethodSymbol IDisposableDispose { get; }
}
}
|