Skip to content

Commit

Permalink
Merge pull request #19 from nsubstitute/GH-18-non-virtual-when
Browse files Browse the repository at this point in the history
  • Loading branch information
tpodolak authored Jul 3, 2018
2 parents 360486e + 6d7ad48 commit ac327ef
Show file tree
Hide file tree
Showing 32 changed files with 10,725 additions and 5 deletions.
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

0 comments on commit ac327ef

Please sign in to comment.