Attribute
is basically a type, as well as enum
. This ambiguity is the standard behaviour. Just free your mind off of class Horse
inheriting Attribute
. In this sense treat it as just a type. Resolving type names is the first thing that the compiler appears to do. Checking attributes (something you attempt to use as an attribute) against compatibility with Attribute
goes after that. Your solution that specifies full name is the only correct.
Update:
Looks like you expect CS compiler to distinguish attribute usage semantics along with resolving type names. One can implement it manually with a custom code analyzer like this:
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public partial class AmbiguityAnalysisAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "AA0001";
private static readonly DiagnosticDescriptor Rule =
new DiagnosticDescriptor(id: DiagnosticId,
title: "Specify the attribute.",
messageFormat: "Possible attribute '{0}' is ambiguous between {1}",
category: "Attribute Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Ambiguous attribute.");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context) =>
context.RegisterSemanticModelAction(SemanticModelAction);
private void SemanticModelAction(SemanticModelAnalysisContext context)
{
var types = GetAllTypes(context.SemanticModel.Compilation).ToArray();
var attributes = GetAllAttribute(context);
var ambiguities = GetAmbiguities(types, attributes);
foreach (var ambiguity in ambiguities)
context.ReportDiagnostic(ambiguity);
}
}
With
public partial class AmbiguityAnalysisAnalyzer
{
private static IEnumerable<INamedTypeSymbol> GetAllTypes(Compilation compilation) =>
GetAllTypes(compilation.GlobalNamespace);
private static IEnumerable<INamedTypeSymbol> GetAllTypes(INamespaceSymbol @namespace)
{
foreach (var type in @namespace.GetTypeMembers())
foreach (var nestedType in GetNestedTypes(type))
yield return nestedType;
foreach (var nestedNamespace in @namespace.GetNamespaceMembers())
foreach (var type in GetAllTypes(nestedNamespace))
yield return type;
}
private static IEnumerable<INamedTypeSymbol> GetNestedTypes(INamedTypeSymbol type)
{
yield return type;
foreach (var nestedType in type.GetTypeMembers()
.SelectMany(nestedType => GetNestedTypes(nestedType)))
yield return nestedType;
}
private static AttributeSyntax[] GetAllAttribute(SemanticModelAnalysisContext context) =>
context
.SemanticModel
.SyntaxTree
.GetRoot()
.DescendantNodes()
.OfType<AttributeSyntax>()
.ToArray();
private static IEnumerable<Diagnostic> GetAmbiguities(INamedTypeSymbol[] types, AttributeSyntax[] attributes)
{
foreach (var attribute in attributes)
{
var usings = GetUsings(attribute.SyntaxTree);
var ambiguities = GetAmbiguities(usings, types, attribute);
if (ambiguities.Length < 2)
continue;
var suggestedAttributes = GetAttributes(ambiguities);
var suggestedNonAttributes = GetNonAttributes(ambiguities);
var parts =
new[]
{
GetPart("attributes", suggestedAttributes),
GetPart("non attributes", suggestedNonAttributes)
}
.Where(part => !part.Equals(string.Empty));
var name = (attribute.Name as IdentifierNameSyntax)?.Identifier.ValueText;
var suggestions =
name == null ?
ImmutableDictionary<string, string>.Empty :
suggestedAttributes.Select(type => GetFullyQualifiedName(type))
.ToImmutableDictionary(type => type, type => name);
var message = string.Join(" and ", parts);
yield return Diagnostic.Create(Rule, attribute.GetLocation(), suggestions, attribute.Name, message);
}
}
}
And other helper methods
public partial class AmbiguityAnalysisAnalyzer
{
private static string GetFullyQualifiedName(INamedTypeSymbol type)
{
var @namespace = GetFullName(type.ContainingNamespace, n => !n.IsGlobalNamespace, n => n.ContainingNamespace);
var name = GetFullName(type, t => t != null, t => t.ContainingType);
if (!@namespace.Equals(string.Empty, StringComparison.Ordinal))
return $"{@namespace}.{name}";
return name;
}
private static string[] GetUsings(SyntaxTree syntaxTree) =>
syntaxTree
.GetCompilationUnitRoot()
.Usings.Select(GetUsingString)
.Concat(new[] { string.Empty })
.ToArray();
private static string GetUsingString(UsingDirectiveSyntax @using) =>
GetUsingStringFromName(@using.Name);
private static string GetUsingStringFromName(NameSyntax name)
{
if (name is IdentifierNameSyntax identifierName)
return identifierName.Identifier.ValueText;
if (name is QualifiedNameSyntax qualifiedName)
return $"{GetUsingStringFromName(qualifiedName.Left)}.{GetUsingStringFromName(qualifiedName.Right)}";
throw new ArgumentException($"Argument '{nameof(name)}' was of unexpected type.");
}
private static INamedTypeSymbol[] GetAmbiguities(IEnumerable<string> usings, IEnumerable<INamedTypeSymbol> types, AttributeSyntax attribute) =>
types
.Where(t => attribute.Name is IdentifierNameSyntax name &&
NameMatches(t, name) &&
NamespaceInUsings(usings, t))
.ToArray();
private static bool NamespaceInUsings(IEnumerable<string> usings, INamedTypeSymbol type) =>
usings.Contains(GetFullName(type.ContainingNamespace, n => !n.IsGlobalNamespace, n => n.ContainingNamespace));
private static bool NameMatches(INamedTypeSymbol type, IdentifierNameSyntax nameSyntax)
{
var isVerbatim = nameSyntax.Identifier.Text.StartsWith("@");
var name = nameSyntax.Identifier.ValueText;
var names = isVerbatim ? new[] { name } : new[] { name, name + "Attribute" };
var fullName = GetFullName(type, t => t != null, t => t.ContainingType);
var res = names.Contains(fullName, StringComparer.Ordinal);
return res;
}
private static string GetFullName<TSymbol>(TSymbol symbol, Func<TSymbol, bool> condition, Func<TSymbol, TSymbol> transition) where TSymbol : ISymbol
{
var values = new List<string>();
while (condition(symbol))
{
values.Add(symbol.Name);
symbol = transition(symbol);
}
values.Reverse();
return string.Join(".", values);
}
private static IEnumerable<INamedTypeSymbol> GetAttributes(IEnumerable<INamedTypeSymbol> types) =>
types.Where(type => IsAttribute(type));
private static IEnumerable<INamedTypeSymbol> GetNonAttributes(IEnumerable<INamedTypeSymbol> types) =>
types.Where(type => !IsAttribute(type));
private static bool IsAttribute(INamedTypeSymbol type) =>
type == null ?
false :
type.ContainingNamespace.Name.Equals("System", StringComparison.Ordinal) &&
type.Name.Equals("Attribute", StringComparison.Ordinal) ||
IsAttribute(type.BaseType);
private static string GetPart(string description, IEnumerable<INamedTypeSymbol> types)
{
var part = string.Join(", ", types.Select(type => $"'{type}'"));
if (!part.Equals(string.Empty))
part = $"{description} {part}";
return part;
}
}
Code fix provider can be the following:
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AmbiguityAnalysisCodeFixProvider)), Shared]
public class AmbiguityAnalysisCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(AmbiguityAnalysisAnalyzer.DiagnosticId);
public sealed override FixAllProvider GetFixAllProvider() =>
WellKnownFixAllProviders.BatchFixer;
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = context.Diagnostics.First();
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var attribute =
root
.FindToken(diagnostic.Location.SourceSpan.Start)
.Parent
.AncestorsAndSelf()
.OfType<AttributeSyntax>()
.First();
foreach(var suggestion in diagnostic.Properties)
{
var title = $"'{suggestion.Value}' to '{suggestion.Key}'";
context.RegisterCodeFix(
CodeAction.Create(
title: title,
createChangedSolution: c => ReplaceAttributeAsync(context.Document, attribute, suggestion.Key, c),
equivalenceKey: title),
diagnostic);
}
}
private static async Task<Solution> ReplaceAttributeAsync(Document document, AttributeSyntax oldAttribute, string suggestion, CancellationToken cancellationToken)
{
var name = SyntaxFactory.ParseName(suggestion);
var newAttribute = SyntaxFactory.Attribute(name);
var root = await document.GetSyntaxRootAsync().ConfigureAwait(false);
root = root.ReplaceNode(oldAttribute, newAttribute);
return document.Project.Solution.WithDocumentSyntaxRoot(document.Id, root);
}
}
Try it with the following code to analyze:
using System;
using Alpha;
using Alpha.Middle;
using Alpha.Middle.Omega;
using Beta;
public class Horse { }
namespace N
{
[Horse]
class C { }
}
namespace Alpha
{
public class Horse : Attribute { }
namespace Middle
{
public class Horse { }
namespace Omega
{
public class Horse : Attribute { }
}
}
}
namespace Beta
{
public enum Horse { }
public class Foo
{
public class Horse : Attribute { }
}
}
It gives errors:
CS0616 'Horse' is not an attribute class
AA0001 Possible attribute 'Horse' is ambiguous between attributes 'Alpha.Horse', 'Alpha.Middle.Omega.Horse' and non attributes 'Horse', 'Alpha.Middle.Horse', 'Beta.Horse'
Suggested fixes are:
'Horse' to 'Alpha.Horse'
'Horse' to 'Alpha.Middle.Omega.Horse'
