12

Background: In an attribute specification, there is sometimes two valid ways to write the applied attribute. For example, if an attribute class has the name HorseAttribute, you can apply the attribute as either [HorseAttribute] or just [Horse]. Ambiguities can be resolved with a @, for example [@Horse].

The following is a valid program:

using System;
using Alpha;
using Beta;

namespace N
{
  [Horse]
  class C
  {
  }
}

namespace Alpha
{
  // valid non-abstract attribute type with accessible constructor
  class HorseAttribute : Attribute
  {
  }
}

namespace Beta
{
  // any non-attribute type with that name
  enum Horse
  {
  }
}

The C# compiler is able to pick Alpha.HorseAttribute when I write just [Horse]. And after all, the type Beta.Horse is entirely unsuitable for use in an attribute specification.

Even if I swap the names, the C# compiler will know what to do:

using System;
using Alpha;
using Beta;

namespace N
{
  [Horse]
  class C
  {
  }
}

namespace Alpha
{
  // valid non-abstract attribute type with accessible constructor
  class Horse : Attribute
  {
  }
}

namespace Beta
{
  // any non-attribute type with that name
  enum HorseAttribute
  {
  }
}

Again, the compiler knows I want Alpha.Horse.


And now for the code I want to ask about. It is identical to the above, except that the two types now have the same name:

using System;
using Alpha;
using Beta;

namespace N
{
  [Horse]
  class C
  {
  }
}

namespace Alpha
{
  // valid non-abstract attribute type with accessible constructor
  class Horse : Attribute
  {
  }
}

namespace Beta
{
  // any non-attribute type with that name
  enum Horse
  {
  }
}

Now, the C#-compiler refuses to build, saying:

error CS0104: 'Horse' is an ambiguous reference between 'Alpha.Horse' and 'Beta.Horse'

Try it online!

My question is, why can the compiler not pick the right one in this case, when it did it nicely in the two examples earlier?

Is this behavior in accordance with the C# Language Specification? Is it actually required that the C# compiler issues an error here?

(Of course I know I can resolve it by saying [Alpha.Horse] explicitly, so I am not asking for that "solution".)

CSDev
  • 3,177
  • 6
  • 19
  • 37
Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • Interesting, see how looks their metadata. – Alexey Medvedev Aug 30 '19 at 09:12
  • 2
    I assume it's an idiosyncrasy of the compiler to do with name resolution. As a guess, the first one works because the compiler looks for `___Attribute` and finds only a single one. The second works because it finds `___Attribute` as an enum and discards it. The third fails because the "let's find duplicates" code doesn't take into account that it has to be an attribute. – DavidG Aug 30 '19 at 09:25
  • 3
    You get CS0104 because the you have imported two types with the same name into the class and your are using that type name. The rest of the attribute stuff is a distraction. The question would be better if both the enum and attribute class were called `HorseAttribute`. https://tio.run/##bY@xbsJAEET7@4opobDooSJpaEiTElGsN6v4JPvO8q5BFvK3m7OJHYToZm9nZ96xZqw8DK368IvvTk2qnXtM@7IuaB4@xJJ2gSrRmljw5W4OOB1io3JOiktSxWdS4zugRuYZl@h/cCQfVmvc0KdV7/rnnKllytpscKEy2UMMGeVqDbGBzBqftyawrhZcvRUgZlH1eSngGJKxZYvNAjEx7Ze7LRb9Rzci/BOMX5sBKHSP@jetVpBhPEtWCW310vOUPQx3 – Jodrell Sep 02 '19 at 13:59

3 Answers3

12

What we have here are two concepts conflated.


1. How the compiler knows what class implements an attribute

There is a simple convention in place that attributes may be referred to by either the class name or the class name less an attribute suffix. So when you add the [Horse] annotation to someIdentifier like this,

[Horse]
someIdentifier

the implementation of [Horse] must be a class that inherits Attribute that is called either HorseAttribute or Horse.

Note: There is a widely accepted convention that all classes that implement attributes should have "Attribute" suffixed to the type name.

2. How the compiler know's which type code is referring to

When we refer to a type, in code, the compiler looks for a definition of that type that has been loaded into the namespace. If there are multiple definitions for that type in the namespace the compiler does nothing to resolve that ambiguity, it is up to the developer to improve the code. The compiler can't choose so raises error CS1040.

The compiler does not do any semantic or static analysis to divine the coders intent. It would be hard to define, costly to perform and prone to error.

This error is not thrown solely when finding implementations for attributes.


In your compiling examples there is no ambiguity around point 2, so the code compiles.

If the resolution of point 1 leads to a type name that is ambiguous be it, Horse or HorseAttribute, then errors will come from point 2.

The compiler does not make special allowances, e.g I'm performing point 2 in response to point 1 so, if I have an ambiguity in this case is there a special fallback position for point 2s performed for point 1s?

If you consider the level of additional complexity and time that special provisions introduce you may accept that it would be better to require a level of stringency from code authors.

In my opinion and, that of others, requiring code that avoids this kind of ambiguity leads to code that is easier to understand by others and one's future self. This makes the discussion of why somewhat moot, as we could argue effort applied here by the compiler team would have enabled "smellier", harder to maintain code.


NOTE: Further to the answer

When you consider behaviour exhibited by the example from the Langauge specification

using System;

[AttributeUsage(AttributeTargets.All)]
public class X: Attribute
{}

[AttributeUsage(AttributeTargets.All)]
public class XAttribute: Attribute
{}

[X]                     // Error: ambiguity
class Class1 {}

[XAttribute]            // Refers to XAttribute
class Class2 {}

[@X]                    // Refers to X
class Class3 {}

[@XAttribute]           // Refers to XAttribute
class Class4 {}

Try here

I would agree there is a confusion and indeed, an inconsistency in the way the compiler treats definitions from one namespace and those imported from different namespaces.

Jodrell
  • 34,946
  • 5
  • 87
  • 124
  • Good explanaton. I guess one can create an analyzer to find out such semantic issues and suggest apropriate fixes with help of `roslyn`. – CSDev Sep 02 '19 at 15:00
  • @Alex, if one thinks it is worth the effort. It may be better to write or, apply an existing, analyzer that ensures attribute implementation names are suffixed with "Attribute" and perhaps another to ensure the inverse. – Jodrell Sep 02 '19 at 15:06
  • And make that analyzer check for situations like `class HorseAttribute : Attribute` vs `enum HorseAttribute`, those are possible (though unlikely). – CSDev Sep 02 '19 at 15:31
  • To test my understanding: If I took my very first example, and added `namespace Gamma { /* any non-attribute type with that name */ delegate int Horse(); }` in the bottom, and also added `using Gamma;` in the top, then what would happen? Because this time there would be _two_ types named just `Horse` plus one type named `HorseAttribute`. What steps would the compiler take, and what would be the end result? – Jeppe Stig Nielsen Sep 02 '19 at 15:50
  • And here is [another example](https://tio.run/##lZAxT8NADIX3@xVvbIeqO50KCwssjIjBcU1zUnKJzk5RVOW3ByelAaQu3Z7P9vfemXXDyuPYaUxHvPVqUu/CpdpXbUnX4lHMdUhUi7bEgtdwDsD7c5NVPlxxRap4cjW9A2pkkXFq4gEvFNNqjTMGbw1h@MuZXWbWdgtKPVKTNmSWY9GZwPpW8BWthJVkmPZ8NCaT/Dmtz/4/ps69i3KQSo7kbcddQPvrxmq985i/Kafv3xtSUlffzHeiyo8yIwq1TGy4xSJmUY1FJeAm@WDH1uTl1P8D4wGLXuyGMI7f) that I find confusing. – Jeppe Stig Nielsen Sep 02 '19 at 17:09
  • @JeppeStigNielsen, that would be fine because, in your first example the attribute is named `HorseAttribute` and the compiler conventionally searches for types named with the "Attribute" suffix first.https://tio.run/##bVG7bsMwDNz1FRyTIcjeoEOaAu2SokDHogOtELYAPQyJTmAU/nZXD8dwW2vSgTze8SjDTgY5jkJYNBRalARv4lsAdEHZGo66bfAwwyfiBXpBYxbwmXSqRiw1hgDv3tUeTcRpXnqBkZWEq1MXOKOym@1UuDekd0UPEh7B0g1Om@1hKg0QP0Oa/vnqfKCvWec0KQxiWK6RrS9W@egDk8n@9vuooqMJ6@wOq8AeJQMye1V1TMB9S3BT3ABKSSGoShNIZ2NjJ9n5WTo7Oc68B5j/655SftlSdIC2L/orstwgQ6LF1rardAyNbGeK3vrofAxRgpwoF9JUYxysLBdqivMXK9@ssP6ldB@zWPXPhuVuycg4/gA – Jodrell Sep 03 '19 at 08:46
0

This is inconsistent behavior of the compiler even though it could be compliant with the specs. IMHO the first scenario should also be an ambiguity error; its not altogether clear that whoever wrote that code was aware that an enum couldn't be used in that context and could be trying something completely different (and wrong obviously). The syntactic sugar that allows you to elide Attribute can run into trouble in scenarios like these.

I'm no expert in how the compiler works, but what I think is happening here is that in one of the very first passes the compiler does over the source code, it must go looking for all elided attributes and replacing them with the type's full name, and woosh, the ambiguity is gone for ever. This doesn't happen when the attribute name isn't elided, as the name isn't replaced and then the ambiguity error crops up on a later step.

InBetween
  • 32,319
  • 3
  • 50
  • 90
  • 1
    The part where you say "IMHO" it should be an ambiguity error even in the first case, does not answer how the language rules are and why they lead to this behavior. By the way I disagree with you; it would be annoying if the first case was an error. Because it is a quite common pattern to have both `Xxx` and `XxxAttribute`. For example there is `SmtpPermission` and `SmtpPermissionAttribute`, different types both in namespace `System.Net.Mail`. Many more examples like this exist in the BCL. If C# was so strict, you could not use these attributes as elegantly. – Jeppe Stig Nielsen Aug 30 '19 at 11:51
  • 1
    I agree with @Jeppe, also not sure this answer gives anything other than opinion. – DavidG Aug 31 '19 at 10:09
0

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'

enter image description here

Community
  • 1
  • 1
CSDev
  • 3,177
  • 6
  • 19
  • 37