2016-06-22 5 views
1

Ich untersuche eine Möglichkeit, Visual Studio eine Warnung auszulösen, wenn ich eine bestimmte Methode in einer Basisklasse überschreibe, aber vergessen, die Basismethode im überschriebenen aufzurufen. Z. B:visuelle Studiowarnung, wenn die Basismethode nicht aufgerufen wird

class Foo 
{ 
    [SomeAttributeToMarkTheMethodToFireTheWarning] 
    public virtual void A() { ... } 
} 

class Bar : Foo 
{ 
    public override void A() 
    { 
     // base.A(); // warning if base.A() is not called 
     // ... 
    } 
} 

Bisher konnte ich nicht einen Weg finden, und wahrscheinlich ist es nicht möglich, den Compiler Feuer eine solche Warnung direkt zu machen. Irgendwelche Ideen für eine Möglichkeit, es zu tun, selbst wenn es ein Tool von Drittanbietern ist oder eine API von der neuen Roslyn .NET Compiler-Plattform verwendet?

UPDATE: Zum Beispiel in Android Studio (IntelliJ), wenn Sie onCreate() in jede Aktivität außer Kraft setzen, aber vergessen, die Basismethode aufrufen super.onCreate(), eine Warnung erhalten. Das ist das Verhalten, das ich in VS brauche.

+0

Aber Sie müssen nicht die Basismethode auslösen. Also gibt es keine Möglichkeit für den Compiler, dies zu tun, da es kein Kompilierungsfehler ist. – Liam

+0

IntelliJ ist Java. Ihre Codierung in C#.Diese Funktionalität existiert nicht in C# – Liam

+0

Ich verstehe, dass mein Code in C# und nicht Java ist und deshalb suche ich nach einem Weg, um das gleiche Ergebnis wie in IntelliJ, aber mit VS –

Antwort

1

Endlich hatte ich etwas Zeit mit Roslyn zu experimentieren und sah aus, als hätte ich eine Lösung mit einem Analysator gefunden. Das ist meine Lösung.

Das Attribut, das Verfahren zu markieren, die in der Unterklasse außer Kraft gesetzt werden muss:

[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] 
public sealed class RequireBaseMethodCallAttribute : Attribute 
{ 
    public RequireBaseMethodCallAttribute() { } 
} 

Der Analysator:

[DiagnosticAnalyzer(LanguageNames.CSharp)] 
public class RequiredBaseMethodCallAnalyzer : DiagnosticAnalyzer 
{ 
    public const string DiagnosticId = "RequireBaseMethodCall"; 

    // You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat. 
    // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Localizing%20Analyzers.md for more on localization 
    private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources)); 
    private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); 
    private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources)); 
    private const string Category = "Usage"; 

    private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); 

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } } 

    public override void Initialize(AnalysisContext context) 
    { 
     context.RegisterCompilationStartAction(AnalyzeMethodForBaseCall); 
    } 

    private static void AnalyzeMethodForBaseCall(CompilationStartAnalysisContext compilationStartContext) 
    { 
     compilationStartContext.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration); 
    } 

    private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) 
    { 
     var mds = context.Node as MethodDeclarationSyntax; 
     if (mds == null) 
     { 
      return; 
     } 

     IMethodSymbol symbol = context.SemanticModel.GetDeclaredSymbol(mds) as IMethodSymbol; 
     if (symbol == null) 
     { 
      return; 
     } 

     if (!symbol.IsOverride) 
     { 
      return; 
     } 

     if (symbol.OverriddenMethod == null) 
     { 
      return; 
     } 

     var overridenMethod = symbol.OverriddenMethod; 
     var attrs = overridenMethod.GetAttributes(); 
     if (!attrs.Any(ad => ad.AttributeClass.MetadataName.ToUpperInvariant() 
          == typeof(RequireBaseMethodCallAttribute).Name.ToUpperInvariant())) 
     { 
      return; 
     } 

     var overridenMethodName = overridenMethod.Name.ToString(); 
     string methodName = overridenMethodName; 

     var invocations = mds.DescendantNodes().OfType<MemberAccessExpressionSyntax>().ToList(); 
     foreach (var inv in invocations) 
     { 
      var expr = inv.Expression; 
      if ((SyntaxKind)expr.RawKind == SyntaxKind.BaseExpression) 
      { 
       var memberAccessExpr = expr.Parent as MemberAccessExpressionSyntax; 
       if (memberAccessExpr == null) 
       { 
        continue; 
       } 

       // compare exprSymbol and overridenMethod 
       var exprMethodName = memberAccessExpr.Name.ToString(); 

       if (exprMethodName != overridenMethodName) 
       { 
        continue; 
       } 

       var invokationExpr = memberAccessExpr.Parent as InvocationExpressionSyntax; 
       if (invokationExpr == null) 
       { 
        continue; 
       } 
       var exprMethodArgs = invokationExpr.ArgumentList.Arguments.ToList(); 
       var ovrMethodParams = overridenMethod.Parameters.ToList(); 

       if (exprMethodArgs.Count != ovrMethodParams.Count) 
       { 
        continue; 
       } 

       var paramMismatch = false; 
       for (int i = 0; i < exprMethodArgs.Count; i++) 
       { 
        var arg = exprMethodArgs[i]; 
        var argType = context.SemanticModel.GetTypeInfo(arg.Expression); 

        var param = arg.NameColon != null ? 
           ovrMethodParams.FirstOrDefault(p => p.Name.ToString() == arg.NameColon.Name.ToString()) : 
           ovrMethodParams[i]; 

        if (param == null || argType.Type != param.Type) 
        { 
         paramMismatch = true; 
         break; 
        } 

        exprMethodArgs.Remove(arg); 
        ovrMethodParams.Remove(param); 
        i--; 
       } 

       // If there are any parameters left without default value 
       // then it is not the base method overload we are looking for 
       if (ovrMethodParams.Any(p => p.HasExplicitDefaultValue)) 
       { 
        continue; 
       } 

       if (!paramMismatch) 
       { 
        // If the actual arguments match with the method params 
        // then the base method invokation was found 
        // and there is no need to continue the search 
        return; 
       } 
      } 
     } 

     var diag = Diagnostic.Create(Rule, mds.GetLocation(), methodName); 
     context.ReportDiagnostic(diag); 
    } 
} 

Der CodeFix Anbieter:

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(BaseMethodCallCodeFixProvider)), Shared] 
public class BaseMethodCallCodeFixProvider : CodeFixProvider 
{ 
    private const string title = "Add base method invocation"; 

    public sealed override ImmutableArray<string> FixableDiagnosticIds 
    { 
     get { return ImmutableArray.Create(RequiredBaseMethodCallAnalyzer.DiagnosticId); } 
    } 

    public sealed override FixAllProvider GetFixAllProvider() 
    { 
     // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers 
     return WellKnownFixAllProviders.BatchFixer; 
    } 

    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 
    { 
     var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 

     var diagnostic = context.Diagnostics.First(); 
     var diagnosticSpan = diagnostic.Location.SourceSpan; 

     // Register a code action that will invoke the fix. 
     context.RegisterCodeFix(
      CodeAction.Create(
       title: title, 
       createChangedDocument: c => AddBaseMethodCallAsync(context.Document, diagnosticSpan, c), 
       equivalenceKey: title), 
      diagnostic); 
    } 

    private async Task<Document> AddBaseMethodCallAsync(Document document, TextSpan diagnosticSpan, CancellationToken cancellationToken) 
    { 
     var root = await document.GetSyntaxRootAsync(cancellationToken); 
     var node = root.FindNode(diagnosticSpan) as MethodDeclarationSyntax; 

     var args = new List<ArgumentSyntax>(); 
     foreach (var param in node.ParameterList.Parameters) 
     { 
      args.Add(SyntaxFactory.Argument(SyntaxFactory.ParseExpression(param.Identifier.ValueText))); 
     } 

     var argsList = SyntaxFactory.SeparatedList(args); 

     var exprStatement = SyntaxFactory.ExpressionStatement(
      SyntaxFactory.InvocationExpression(
       SyntaxFactory.MemberAccessExpression(
        SyntaxKind.SimpleMemberAccessExpression, 
        SyntaxFactory.BaseExpression(), 
        SyntaxFactory.Token(SyntaxKind.DotToken), 
        SyntaxFactory.IdentifierName(node.Identifier.ToString()) 
       ), 
       SyntaxFactory.ArgumentList(argsList) 
      ), 
      SyntaxFactory.Token(SyntaxKind.SemicolonToken) 
     ); 

     var newBodyStatements = SyntaxFactory.Block(node.Body.Statements.Insert(0, exprStatement)); 
     var newRoot = root.ReplaceNode(node.Body, newBodyStatements).WithAdditionalAnnotations(Simplifier.Annotation); 

     return document.WithSyntaxRoot(newRoot); 
    } 
} 

Und eine Demo wie es funktioniert: http://screencast.com/t/4Jgm989TI

Da ich völlig neu in der .NET Compiler Platform bin, hätte ich gerne Feedback und Vorschläge, wie ich meine Lösung verbessern kann. Vielen Dank im Voraus!

3

Wenn Sie einen Code, um sicherzustellen, wollen ausgeführt wird, dann sollten Sie Ihr Design ändern:

abstract class Foo 
{ 
    protected abstract void PostA(); 

    public void A() { 
     ... 
     PostA(); 
    } 
} 


class Bar : Foo 
{ 
    protected override void PostA() 
    { 

    } 
} 

//method signature remains the same: 
Bar.A(); 

Auf diese Weise A() wird immer vor Ihrer überschriebene Methode gebrannt

mehr inheritence zu haben und A, um sicherzustellen,() aufgerufen, Sie müssten Bar abstrakt machen auch:

abstract class Bar : Foo 
{ 
    //no need to override now 
} 

class Baz:Bar 
{ 
    protected override void PostA() 
    { 

    } 
} 

Es gibt keine Möglichkeit genau das zu tun was willst du in C#. Dies ist kein Visual Studio-Problem. So funktioniert C#.

Virtuelle Methodensignaturen können außer Kraft gesetzt werden oder nicht, in der Basis aufgerufen oder nicht. Sie haben zwei Optionen virtuell oder abstrakt. Ihre Verwendung virtual und ich habe Ihnen eine abstract Lösung gegeben. Es liegt an Ihnen zu wählen, welche Sie verwenden möchten.

Die nächste Sache, die ich von was Sie wollen, wäre ein #warning wäre. Siehe this answer. Dies erzeugt jedoch nur die Warnung im Ausgabefenster, nicht in Intellisense. Grundsätzlich C# does not support custom compiler warnings.

+0

Plus, absolut zu erreichen. Kannst du verhindern, dass 'A' in C# überschrieben wird? – Bathsheba

+0

Das Problem ist, dass meine PostA-Methode in der Basisklasse nicht abstrakt sein kann, da es einen Rumpf hat. –

+4

Sie legen den Körper in A(). Nur der Code, den Sie überschreiben möchten, geht in PostA() – Liam

Verwandte Themen