5

This question is not about a method I can mark with [System.Obsolete]. The method I wanna ignore is in a dll I don't have control over.

I use a 3rd party library which contains an extension method for objects. This leads to confusion and may cause problems in the future. Is there any way to mark this extension method (or all the extension methods from a certain dll ) as obsolete externally or prevent this extension method appearing in intellisense. The problematic method is :

    public static class ExtensionMethods
    {
      public static bool IsNumeric(this object obj)
      {
        if (obj == null)
          return false;
        return obj.GetType().IsPrimitive || obj is double || (obj is Decimal || obj is DateTime) || obj is TimeSpan;
      }
    }
yey
  • 123
  • 9
  • is it in a namespace that you can safely ignore? – Daniel A. White Aug 04 '17 at 13:50
  • @DanielA.White yes, but I just want to ignore the extension methods, not the whole dll or namespace – yey Aug 04 '17 at 14:07
  • 1
    Is the linked question really a duplicate? That question is about marking a method in your *own* assembly, that you control. This question is about marking in a 3rd party assembly, that you have no control over. I've voted to re-open. – Bradley Uffner Aug 04 '17 at 14:18
  • The best way to do this would be with a [Live Code Analyzer](https://learn.microsoft.com/en-us/visualstudio/extensibility/getting-started-with-roslyn-analyzers). Unfortunately, they are non-trivial to create, though this would be one of the more simple ones. – Bradley Uffner Aug 04 '17 at 14:22
  • @BradleyUffner yep, this is not a duplicate. Thanks – yey Aug 04 '17 at 14:33
  • 1
    I've decided to use this opportunity to try and create my first live code analyzer. It is going... less than smoothly, but I'll post the code for you once it is complete. What is the full name (including namespace, class, and method name) of the method you want to block? – Bradley Uffner Aug 04 '17 at 17:37
  • @BradleyUffner Good luck! I look forward to seeing this, since I will probably tackle this topic myself at work. BTW, I think you should also consider the library if you want to make it as robust as possible. – Kapol Aug 04 '17 at 18:18
  • Your timing is great, I just posted a simple, hard-coded, version. I may expand this in to a full plugin at some point, as I've wanted this functionality myself. – Bradley Uffner Aug 04 '17 at 18:36

2 Answers2

5

You can do this with a Roslyn Code Analyzer. The following code will create a DiagnosticAnalyzer that will give a compiler warning if String.EndsWith() is used.

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ForbiddenMethodsAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor("Forbidden",
                                                                                 "Don't use this method!",
                                                                                 "Use of the '{0}' method is not allowed",
                                                                                 "Forbidden.Stuff",
                                                                                 DiagnosticSeverity.Warning,
                                                                                 isEnabledByDefault: true,
                                                                                 description: "This method is forbidden");
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.InvocationExpression);
    }

    private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
    {
        var invocationExpression = (InvocationExpressionSyntax)context.Node;
        var memberAccessExpression = invocationExpression.Expression as MemberAccessExpressionSyntax;
        if (memberAccessExpression?.Name.ToString() == "EndsWith")
        {
            var memberSymbol = context.SemanticModel.GetSymbolInfo(memberAccessExpression).Symbol as IMethodSymbol;
            var containingType = memberSymbol.ContainingType;
            if (containingType.ContainingNamespace.Name == "System" && containingType.Name == "String")
            {
                var diagnostic = Diagnostic.Create(Rule, invocationExpression.GetLocation(), memberAccessExpression.ToString());
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
}

Tooltip warning Error List warning

There are 3 options to use an Analyzer like this:

  • Add the DiagnosticAnalyzer code directly to your project. It will apply only to that solution.
  • Create a class library with the DiagnosticAnalyzer in it, and distribute it as a Nuget package. It will apply only to solutions that use the package.
  • Compile a full VSIX extension containing the class. The analyzer will work on any solution you load.

This is the first project I've done that uses the Roslyn Code Analysis functionality, so unfortunately I don't understand everything that is going on here. I started with the default Analyzer template and tried various methods, stepped through code, and looked at variables with the watch windows until I found the information I needed for this functionality.

The basic process is to register a SyntaxNode Analysis function, filtered to expressions that invoke a method. Within that method I check to see if the Name of the MemberAccessExpressionSyntax being examined is "EndsWith". If it is, I get the ContainingType that the method is on, and check to see if it is on the String class in the System namespace. If it is, I create a Diagnostic instance from a DiagnosticDescriptor to tell the IDE where the problem is, and how much of a problem it represents (A warning in this case, I could make it a full Error if I wanted, which would prevent the code from compiling). It is also possible to present the user with different options to automatically fix the error, but I haven't explored that yet.

A lot of the information came from this tutorial, as well as a whole lot of trial and error.

Bradley Uffner
  • 16,641
  • 3
  • 39
  • 76
  • If you have access to Pluralsight, they have a very nice course called *Introduction to the .NET Compiler Platform*. It is a little bit out of date here and there, but it doesn't make it any less useful. – Kapol Aug 04 '17 at 19:09
  • You might want to add that if it is strictly forbidden to use that method, the severity can be changed to make the code non-compilable. – Kapol Aug 04 '17 at 19:11
4

The best way to handle this would be to use Roslyn and create your own code analyzer, or use an existing tool like FxCop.

However, I found a very non-elegant workaround for this.

In your project you can create a class with the same name as the referenced class, residing in an identical namespace, with the exact same method. Now mark your method as obsolete.

The below code sample has a reference to a library with an ExtensionMethods class which is defined in the External namespace. In the line marked with (*) comment, where the method is called using the static method call syntax, the compiler warns you that the type ExtensionMethods conflicts with an imported type. It also tells you that the method is obsolete (since you have shadowed the imported type, it sees your definition). So when you invoke the method, your code will run. In the line marked with (**) comment, where the method is called using the extension method call syntax, the compiler says that the call is ambiguous and the code won't compile. The only workaround I know of is to turn this call into line (*), which will produce the obsolete warning.

With this solution you will be able to call other extension methods from the referenced type if you use the extension method syntax, provided you don't have the same method defined in your class.

using System;
using External;

namespace Internal
{
    class Program
    {
        static void Main(string[] args)
        {
            ExtensionMethods.IsNumeric(new object()); // (*)
            new object().IsNumeric(); // (**)
        }
    }
}

namespace External
{
    public static class ExtensionMethods
    {
        [Obsolete]
        public static bool IsNumeric(this object o)
        {
            if (obj == null)
              return false;
            return obj.GetType().IsPrimitive || obj is double || (obj is Decimal || obj is DateTime) || obj is TimeSpan;
        }
    }
}
Kapol
  • 6,383
  • 3
  • 21
  • 46