Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GH-16] - non virtual received calls #17

Merged
merged 9 commits into from
Jun 30, 2018
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using NSubstitute.Analyzers.Shared;
using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers;

namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal class NonVirtualSetupReceivedAnalyzer : AbstractNonVirtualSetupReceivedAnalyzer<SyntaxKind>
{
protected override ImmutableArray<Parent> PossibleParents { get; } = ImmutableArray.Create(
Parent.Create<MemberAccessExpressionSyntax>(),
Parent.Create<InvocationExpressionSyntax>(),
Parent.Create<ElementAccessExpressionSyntax>());

protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression;

public NonVirtualSetupReceivedAnalyzer()
: base(new DiagnosticDescriptorsProvider())
{
}
}
}
33 changes: 30 additions & 3 deletions src/NSubstitute.Analyzers.CSharp/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/NSubstitute.Analyzers.CSharp/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -249,4 +249,16 @@
<value>Can not provide constructor arguments when substituting for a delegate.</value>
<comment>The title of the diagnostic.</comment>
</data>
<data name="NonVirtualReceivedSetupSpecificationDescription" xml:space="preserve">
<value>Non-virtual members can not be intercepted.</value>
<comment>An optional longer localizable description of the diagnostic.</comment>
</data>
<data name="NonVirtualReceivedSetupSpecificationMessageFormat" xml:space="preserve">
<value>Member {0} can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted.</value>
<comment>The format-able message the diagnostic displays.</comment>
</data>
<data name="NonVirtualReceivedSetupSpecificationTitle" xml:space="preserve">
<value>Non-virtual setup specification.</value>
<comment>The title of the diagnostic.</comment>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ internal class AbstractDiagnosticDescriptorsProvider<T> : IDiagnosticDescriptors
public DiagnosticDescriptor SubstituteConstructorArgumentsForInterface { get; } = DiagnosticDescriptors<T>.SubstituteConstructorArgumentsForInterface;

public DiagnosticDescriptor SubstituteConstructorArgumentsForDelegate { get; } = DiagnosticDescriptors<T>.SubstituteConstructorArgumentsForDelegate;

public DiagnosticDescriptor NonVirtualReceivedSetupSpecification { get; } = DiagnosticDescriptors<T>.NonVirtualReceivedSetupSpecification;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using NSubstitute.Analyzers.Shared.Extensions;

namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers
{
Expand Down Expand Up @@ -48,12 +49,7 @@ public sealed override void Initialize(AnalysisContext context)
return false;
}

if (symbolInfo.Symbol == null)
{
return null;
}

return IsInterfaceMember(symbolInfo) || IsVirtual(symbolInfo);
return symbolInfo.Symbol?.CanBeSetuped();
}

private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext)
Expand Down Expand Up @@ -134,21 +130,5 @@ private bool IsValidForAnalysis(SyntaxNode accessedMember)
{
return accessedMember != null && SupportedMemberAccesses.Contains(accessedMember.RawKind);
}

private bool IsInterfaceMember(SymbolInfo symbolInfo)
{
return symbolInfo.Symbol?.ContainingType?.TypeKind == TypeKind.Interface;
}

private bool IsVirtual(SymbolInfo symbolInfo)
{
var member = symbolInfo.Symbol;

var isVirtual = member.IsVirtual
|| (member.IsOverride && !member.IsSealed)
|| member.IsAbstract;

return isVirtual;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using NSubstitute.Analyzers.Shared.Extensions;

namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers
{
internal abstract class AbstractNonVirtualSetupReceivedAnalyzer<TSyntaxKind> : AbstractDiagnosticAnalyzer
where TSyntaxKind : struct
{
private static readonly ImmutableHashSet<string> MethodNames = ImmutableHashSet.Create(
MetadataNames.NSubstituteReceivedMethod,
MetadataNames.NSubstituteReceivedWithAnyArgsMethod,
MetadataNames.NSubstituteDidNotReceiveMethod,
MetadataNames.NSubstituteDidNotReceiveWithAnyArgsMethod);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptorsProvider.NonVirtualReceivedSetupSpecification);

protected AbstractNonVirtualSetupReceivedAnalyzer(IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider)
: base(diagnosticDescriptorsProvider)
{
}

protected abstract ImmutableArray<Parent> PossibleParents { get; }

protected abstract TSyntaxKind InvocationExpressionKind { get; }

public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeInvocation, InvocationExpressionKind);
}

private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext)
{
var invocationExpression = syntaxNodeContext.Node;
var methodSymbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(invocationExpression);

if (methodSymbolInfo.Symbol?.Kind != SymbolKind.Method)
{
return;
}

var methodSymbol = (IMethodSymbol)methodSymbolInfo.Symbol;
if (methodSymbol == null)
{
return;
}

if (IsReceivedLikeMethod(syntaxNodeContext, invocationExpression, methodSymbol.Name) == false)
{
return;
}

var parentNode = GetKnownParent(invocationExpression);

if (parentNode == null)
{
return;
}

var symbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(parentNode);

if (symbolInfo.Symbol == null)
{
return;
}

if (symbolInfo.Symbol.CanBeSetuped())
{
return;
}

var diagnostic = Diagnostic.Create(
DiagnosticDescriptorsProvider.NonVirtualReceivedSetupSpecification,
invocationExpression.GetLocation(),
symbolInfo.Symbol.Name);

syntaxNodeContext.ReportDiagnostic(diagnostic);
}

private static bool IsReceivedLikeMethod(SyntaxNodeAnalysisContext syntaxNodeContext, SyntaxNode syntax, string memberName)
{
if (MethodNames.Contains(memberName) == false)
{
return false;
}

var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(syntax);

return symbol.Symbol?.ContainingAssembly?.Name.Equals(MetadataNames.NSubstituteAssemblyName, StringComparison.Ordinal) == true &&
symbol.Symbol?.ContainingType?.ToString().Equals(MetadataNames.NSubstituteSubstituteExtensionsFullTypeName, StringComparison.Ordinal) == true;
}

private SyntaxNode GetKnownParent(SyntaxNode receivedSyntaxNode)
{
var typeInfo = receivedSyntaxNode.Parent.GetType().GetTypeInfo();

if (PossibleParents.Any(parent => parent.Type.GetTypeInfo().IsAssignableFrom(typeInfo)))
{
return receivedSyntaxNode.Parent;
}

return null;
}
}
}
8 changes: 8 additions & 0 deletions src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ internal class DiagnosticDescriptors<T>
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static DiagnosticDescriptor NonVirtualReceivedSetupSpecification { get; } =
CreateDiagnosticDescriptor(
name: nameof(NonVirtualReceivedSetupSpecification),
id: DiagnosticIdentifiers.NonVirtualReceivedSetupSpecification,
category: DiagnosticCategories.Usage,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

private static DiagnosticDescriptor CreateDiagnosticDescriptor(
string name, string id, string category, DiagnosticSeverity defaultSeverity, bool isEnabledByDefault)
{
Expand Down
1 change: 1 addition & 0 deletions src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ internal class DiagnosticIdentifiers
public static readonly string SubstituteMultipleClasses = "NS008";
public static readonly string SubstituteConstructorArgumentsForInterface = "NS009";
public static readonly string SubstituteConstructorArgumentsForDelegate = "NS010";
public static readonly string NonVirtualReceivedSetupSpecification = "NS011";
}
}
22 changes: 21 additions & 1 deletion src/NSubstitute.Analyzers.Shared/Extensions/ISymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ namespace NSubstitute.Analyzers.Shared.Extensions
{
internal static class ISymbolExtensions
{
public static bool CanBeSetuped(this ISymbol symbol)
{
return IsInterfaceMember(symbol) || IsVirtual(symbol);
}

public static bool InternalsVisibleToProxyGenerator(this ISymbol typeSymbol)
{
var internalsVisibleToAttribute = typeSymbol.ContainingAssembly.GetAttributes()
Expand All @@ -23,9 +28,24 @@ public static string ToMinimalMethodString(this ISymbol symbol, SemanticModel se
return string.Empty;
}

var minimumDisplayString = symbol.ToMinimalDisplayString(semanticModel, 0, SymbolDisplayFormat.FullyQualifiedFormat);
var minimumDisplayString =
symbol.ToMinimalDisplayString(semanticModel, 0, SymbolDisplayFormat.FullyQualifiedFormat);

return $"{symbol.ContainingType}.{minimumDisplayString}";
}

private static bool IsInterfaceMember(ISymbol symbol)
{
return symbol?.ContainingType?.TypeKind == TypeKind.Interface;
}

private static bool IsVirtual(ISymbol symbol)
{
var isVirtual = symbol.IsVirtual
|| (symbol.IsOverride && !symbol.IsSealed)
|| symbol.IsAbstract;

return isVirtual;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ internal interface IDiagnosticDescriptorsProvider
DiagnosticDescriptor SubstituteConstructorArgumentsForInterface { get; }

DiagnosticDescriptor SubstituteConstructorArgumentsForDelegate { get; }

DiagnosticDescriptor NonVirtualReceivedSetupSpecification { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.VisualBasic;
using Microsoft.CodeAnalysis.VisualBasic.Syntax;
using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers;

namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers
{
[DiagnosticAnalyzer(LanguageNames.VisualBasic)]
internal class NonVirtualSetupReceivedAnalyzer : AbstractNonVirtualSetupReceivedAnalyzer<SyntaxKind>
{
protected override ImmutableArray<Parent> PossibleParents { get; } = ImmutableArray.Create(
Parent.Create<MemberAccessExpressionSyntax>(),
Parent.Create<InvocationExpressionSyntax>());

protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression;

public NonVirtualSetupReceivedAnalyzer()
: base(new DiagnosticDescriptorsProvider())
{
}
}
}
Loading