Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making extension methods from a third party library obsolete

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;
      }
    }
like image 317
yey Avatar asked Aug 04 '17 13:08

yey


2 Answers

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.

like image 136
Bradley Uffner Avatar answered Sep 30 '22 10:09

Bradley Uffner


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;
        }
    }
}
like image 21
Kapol Avatar answered Sep 30 '22 08:09

Kapol