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 18 non virtual when #19

Merged
merged 8 commits into from
Jul 3, 2018
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers;

namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal class NonVirtualSetupWhenAnalyzer : AbstractNonVirtualWhenAnalyzer<SyntaxKind, InvocationExpressionSyntax>
{
public NonVirtualSetupWhenAnalyzer()
: base(new DiagnosticDescriptorsProvider())
{
}

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

protected override IEnumerable<SyntaxNode> GetExpressionsForAnalysys(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, IMethodSymbol methodSymbol, InvocationExpressionSyntax invocationExpressionSyntax)
{
var argumentListArguments = invocationExpressionSyntax.ArgumentList.Arguments;
var argumentSyntax = methodSymbol.MethodKind == MethodKind.ReducedExtension ? argumentListArguments.First() : argumentListArguments.Skip(1).First();
return GetExpressionsForAnalysys(syntaxNodeAnalysisContext, argumentSyntax.Expression);
}

private IEnumerable<SyntaxNode> GetExpressionsForAnalysys(SyntaxNodeAnalysisContext syntaxNodeContext, SyntaxNode argumentSyntax)
{
SyntaxNode body = null;
switch (argumentSyntax)
{
case SimpleLambdaExpressionSyntax simpleLambdaExpressionSyntax:
body = simpleLambdaExpressionSyntax.Body;
break;
case AnonymousFunctionExpressionSyntax anonymousFunctionExpressionSyntax:
body = anonymousFunctionExpressionSyntax.Body;
break;
case LocalFunctionStatementSyntax localFunctionStatementSyntax:
body = (SyntaxNode)localFunctionStatementSyntax.Body ?? localFunctionStatementSyntax.ExpressionBody;
break;
case MethodDeclarationSyntax methodDeclarationSyntax:
body = (SyntaxNode)methodDeclarationSyntax.Body ?? methodDeclarationSyntax.ExpressionBody;
break;
case IdentifierNameSyntax identifierNameSyntax:
var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(identifierNameSyntax);
if (symbol.Symbol != null && symbol.Symbol.Locations.Any())
{
var location = symbol.Symbol.Locations.First();
var syntaxNode = location.SourceTree.GetRoot().FindNode(location.SourceSpan);

foreach (var expressionsForAnalysy in GetExpressionsForAnalysys(syntaxNodeContext, syntaxNode))
{
yield return expressionsForAnalysy;
}
}

break;
}

if (body == null)
{
yield break;
}

foreach (var invocationExpressionSyntax in body.DescendantNodes().Where(node => node.IsKind(SyntaxKind.SimpleMemberAccessExpression) ||
node.IsKind(SyntaxKind.ElementAccessExpression)))
{
yield return invocationExpressionSyntax;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard1.1</TargetFramework>
<TargetFramework>netstandard1.3</TargetFramework>
<PackageTargetFallback>portable45-net45+win8</PackageTargetFallback>
<IncludeBuildOutput>false</IncludeBuildOutput>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
Expand All @@ -22,7 +22,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Update="NetStandard.Library" Version="$(NetStandardImplicitPackageVersion)" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="1.3.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.8.2" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<None Update="tools\*.ps1" CopyToOutputDirectory="Always" Pack="true" PackagePath="" />
Expand Down
27 changes: 27 additions & 0 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 @@ -261,4 +261,16 @@
<value>Non-virtual setup specification.</value>
<comment>The title of the diagnostic.</comment>
</data>
<data name="NonVirtualWhenSetupSpecificationDescription" 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="NonVirtualWhenSetupSpecificationMessageFormat" 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="NonVirtualWhenSetupSpecificationTitle" 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 @@ -27,5 +27,7 @@ internal class AbstractDiagnosticDescriptorsProvider<T> : IDiagnosticDescriptors
public DiagnosticDescriptor SubstituteConstructorArgumentsForDelegate { get; } = DiagnosticDescriptors<T>.SubstituteConstructorArgumentsForDelegate;

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

public DiagnosticDescriptor NonVirtualWhenSetupSpecification { get; } = DiagnosticDescriptors<T>.NonVirtualWhenSetupSpecification;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using NSubstitute.Analyzers.Shared.Extensions;

namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers
{
internal abstract class AbstractNonVirtualWhenAnalyzer<TSyntaxKind, TInvocationExpressionSyntax> : AbstractDiagnosticAnalyzer
where TInvocationExpressionSyntax : SyntaxNode
where TSyntaxKind : struct
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptorsProvider.NonVirtualWhenSetupSpecification);

private static readonly ImmutableHashSet<string> MethodNames = ImmutableHashSet.Create(
MetadataNames.NSubstituteWhenMethod,
MetadataNames.NSubstituteWhenForAnyArgsMethod);

protected abstract TSyntaxKind InvocationExpressionKind { get; }

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

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

protected abstract IEnumerable<SyntaxNode> GetExpressionsForAnalysys(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, IMethodSymbol methodSymbol, TInvocationExpressionSyntax invocationExpressionSyntax);

private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext)
{
var invocationExpression = (TInvocationExpressionSyntax)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 (IsWhenLikeMethod(syntaxNodeContext, invocationExpression, methodSymbol.Name) == false)
{
return;
}

var expressionsForAnalysys = GetExpressionsForAnalysys(syntaxNodeContext, methodSymbol, invocationExpression);
var typeSymbol = methodSymbol.TypeArguments.FirstOrDefault() ?? methodSymbol.ReceiverType;
foreach (var analysedSyntax in expressionsForAnalysys)
{
var symbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(analysedSyntax);
if (symbolInfo.Symbol != null && symbolInfo.Symbol.ContainingType == typeSymbol && symbolInfo.Symbol.CanBeSetuped() == false)
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptorsProvider.NonVirtualWhenSetupSpecification,
analysedSyntax.GetLocation(),
symbolInfo.Symbol.Name);

syntaxNodeContext.ReportDiagnostic(diagnostic);
}
}
}

private bool IsWhenLikeMethod(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.OrdinalIgnoreCase) == true &&
symbol.Symbol?.ContainingType?.ToString().Equals(MetadataNames.NSubstituteSubstituteExtensionsFullTypeName, StringComparison.OrdinalIgnoreCase) == true;
}
}
}
8 changes: 8 additions & 0 deletions src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ internal class DiagnosticDescriptors<T>
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static DiagnosticDescriptor NonVirtualWhenSetupSpecification { get; } =
CreateDiagnosticDescriptor(
name: nameof(NonVirtualWhenSetupSpecification),
id: DiagnosticIdentifiers.NonVirtualWhenSetupSpecification,
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 @@ -13,5 +13,6 @@ internal class DiagnosticIdentifiers
public static readonly string SubstituteConstructorArgumentsForInterface = "NS009";
public static readonly string SubstituteConstructorArgumentsForDelegate = "NS010";
public static readonly string NonVirtualReceivedSetupSpecification = "NS011";
public static readonly string NonVirtualWhenSetupSpecification = "NS012";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ internal interface IDiagnosticDescriptorsProvider
DiagnosticDescriptor SubstituteConstructorArgumentsForDelegate { get; }

DiagnosticDescriptor NonVirtualReceivedSetupSpecification { get; }

DiagnosticDescriptor NonVirtualWhenSetupSpecification { get; }
}
}
2 changes: 2 additions & 0 deletions src/NSubstitute.Analyzers.Shared/MetadataNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ internal class MetadataNames
public const string NSubstituteForPartsOfMethod = "ForPartsOf";
public const string InternalsVisibleToAttributeFullTypeName = "System.Runtime.CompilerServices.InternalsVisibleToAttribute";
public const string CastleDynamicProxyGenAssembly2Name = "DynamicProxyGenAssembly2";
public const string NSubstituteWhenMethod = "When";
public const string NSubstituteWhenForAnyArgsMethod = "WhenForAnyArgs";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System.Collections.Generic;
using System.Linq;
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 NonVirtualSetupWhenAnalyzer : AbstractNonVirtualWhenAnalyzer<SyntaxKind, InvocationExpressionSyntax>
{
public NonVirtualSetupWhenAnalyzer()
: base(new DiagnosticDescriptorsProvider())
{
}

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

protected override IEnumerable<SyntaxNode> GetExpressionsForAnalysys(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, IMethodSymbol methodSymbol, InvocationExpressionSyntax invocationExpressionSyntax)
{
var argumentListArguments = invocationExpressionSyntax.ArgumentList.Arguments;
var argumentSyntax = methodSymbol.MethodKind == MethodKind.ReducedExtension ? argumentListArguments.First() : argumentListArguments.Skip(1).First();
return GetExpressionsForAnalysys(syntaxNodeAnalysisContext, argumentSyntax.GetExpression());
}

private IEnumerable<SyntaxNode> GetExpressionsForAnalysys(SyntaxNodeAnalysisContext syntaxNodeContext, SyntaxNode syntax)
{
SyntaxNode body = null;
switch (syntax)
{
case SingleLineLambdaExpressionSyntax _:
case ExpressionStatementSyntax _:
case LocalDeclarationStatementSyntax _:
case AssignmentStatementSyntax _:
body = syntax;
break;
case MultiLineLambdaExpressionSyntax simpleLambdaExpressionSyntax:
foreach (var syntaxNode in IterateStatements(simpleLambdaExpressionSyntax.Statements))
{
yield return syntaxNode;
}

break;
case MethodBlockSyntax methodBlockSyntax:
foreach (var syntaxNode in IterateStatements(methodBlockSyntax.Statements))
{
yield return syntaxNode;
}

break;
case UnaryExpressionSyntax unaryExpressionSyntax:
foreach (var syntaxNode in GetExpressionsForAnalysys(syntaxNodeContext, unaryExpressionSyntax.Operand))
{
yield return syntaxNode;
}

break;
case IdentifierNameSyntax identifierNameSyntax:
var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(identifierNameSyntax);
if (symbol.Symbol != null && symbol.Symbol.Locations.Any())
{
var location = symbol.Symbol.Locations.First();
var syntaxNode = location.SourceTree.GetRoot().FindNode(location.SourceSpan);

SyntaxNode innerNode = null;
if (syntaxNode is MethodStatementSyntax methodStatementSyntax)
{
innerNode = methodStatementSyntax.Parent;
}

innerNode = innerNode ?? syntaxNode;
foreach (var expressionsForAnalysy in GetExpressionsForAnalysys(syntaxNodeContext, innerNode))
{
yield return expressionsForAnalysy;
}
}

break;
}

if (body == null)
{
yield break;
}

var memberAccessExpressions = body.DescendantNodes().Where(node => node.IsKind(SyntaxKind.SimpleMemberAccessExpression));
var invocationExpressions = body.DescendantNodes().Where(node => node.IsKind(SyntaxKind.InvocationExpression));

// rather ugly but prevents reporting two times the same thing
// as VB syntax is based on statements, you can't access body of method directly
if (invocationExpressions.Any())
{
foreach (var invocationExpression in invocationExpressions)
{
yield return invocationExpression;
}
}
else if (memberAccessExpressions.Any())
{
foreach (var memberAccessExpression in memberAccessExpressions)
{
yield return memberAccessExpression;
}
}

IEnumerable<SyntaxNode> IterateStatements(IEnumerable<StatementSyntax> statements)
{
return statements.SelectMany(statement => GetExpressionsForAnalysys(syntaxNodeContext, statement));
}
}
}
}
Loading