diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs index 12a8407b..572b9cef 100644 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs @@ -1,311 +1,84 @@ -using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; 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 CallInfoAnalyzer : AbstractDiagnosticAnalyzer + internal class CallInfoAnalyzer : AbstractCallInfoAnalyzer { public CallInfoAnalyzer() : base(new DiagnosticDescriptorsProvider()) { } - private static readonly ImmutableHashSet MethodNames = ImmutableHashSet.Create( - MetadataNames.NSubstituteReturnsMethod, - MetadataNames.NSubstituteReturnsForAnyArgsMethod); + protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( - DiagnosticDescriptorsProvider.CallInfoArgumentOutOfRange, - DiagnosticDescriptorsProvider.CallInfoCouldNotConvertParameterAtPosition, - DiagnosticDescriptorsProvider.CallInfoCouldNotFindArgumentToThisCall, - DiagnosticDescriptorsProvider.CallInfoMoreThanOneArgumentOfType, - DiagnosticDescriptorsProvider.CallInfoArgumentSetWithIncompatibleValue, - DiagnosticDescriptorsProvider.CallInfoArgumentIsNotOutOrRef); - - public override void Initialize(AnalysisContext context) + protected override SyntaxNode GetParentMethodCall(InvocationExpressionSyntax invocationExpressionSyntax) { - context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + return invocationExpressionSyntax.Expression.DescendantNodes().First(); } - private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext) + protected override IEnumerable GetArgumentExpressions(InvocationExpressionSyntax invocationExpressionSyntax) { - var invocationExpression = (InvocationExpressionSyntax)syntaxNodeContext.Node; - var methodSymbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(invocationExpression); - - if (methodSymbolInfo.Symbol?.Kind != SymbolKind.Method) - { - return; - } - - var methodSymbol = (IMethodSymbol)methodSymbolInfo.Symbol; - if (SupportsCallInfo(syntaxNodeContext, invocationExpression, methodSymbol) == false) - { - return; - } - - var memberAccessExpression = invocationExpression.Expression.DescendantNodes().First(); - - var parentCallInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(memberAccessExpression).Symbol as IMethodSymbol; - if (parentCallInfo == null) - { - return; - } - - foreach (var argument in invocationExpression.ArgumentList.Arguments) - { - var finder = new CallInfoCallFinder(); - var callInfoContext = finder.GetCallInfoContext(syntaxNodeContext.SemanticModel, argument.Expression); - foreach (var argAtInvocation in callInfoContext.ArgAtInvocations) - { - var position = syntaxNodeContext.SemanticModel.GetConstantValue(argAtInvocation.ArgumentList.Arguments.First().Expression); - if (position.HasValue && position.Value is int intPosition) - { - if (intPosition > parentCallInfo.Parameters.Length - 1) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.CallInfoArgumentOutOfRange, - argAtInvocation.GetLocation(), - position); - - syntaxNodeContext.ReportDiagnostic(diagnostic); - continue; - } - - var symbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(argAtInvocation); - if (symbolInfo.Symbol != null && - symbolInfo.Symbol is IMethodSymbol argAtMethodSymbol && - Equals(parentCallInfo.Parameters[intPosition].Type, argAtMethodSymbol.TypeArguments.First()) == false) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.CallInfoCouldNotConvertParameterAtPosition, - argAtInvocation.GetLocation(), - position, - argAtMethodSymbol.TypeArguments.First()); - - syntaxNodeContext.ReportDiagnostic(diagnostic); - } - } - } - - foreach (var argInvocation in callInfoContext.ArgInvocations) - { - var symbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(argInvocation); - if (symbolInfo.Symbol != null && symbolInfo.Symbol is IMethodSymbol argMethodSymbol) - { - var typeSymbol = argMethodSymbol.TypeArguments.First(); - var parameterCount = parentCallInfo.Parameters.Count(param => Equals(param.Type, typeSymbol)); - if (parameterCount == 0) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.CallInfoCouldNotFindArgumentToThisCall, - argInvocation.GetLocation(), - typeSymbol); - - syntaxNodeContext.ReportDiagnostic(diagnostic); - continue; - } - - if (parameterCount > 1) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.CallInfoMoreThanOneArgumentOfType, - argInvocation.GetLocation(), - typeSymbol); - - syntaxNodeContext.ReportDiagnostic(diagnostic); - } - } - } - - foreach (var indexer in callInfoContext.IndexerAccesses) - { - var info = syntaxNodeContext.SemanticModel.GetSymbolInfo(indexer.Parent.DescendantNodes().First()); - var symbol = info.Symbol as IMethodSymbol; - var verifyIndexerCast = symbol == null || symbol.Name != MetadataNames.CallInfoArgTypesMethod; - var verifyAssignment = symbol == null; - - var position = syntaxNodeContext.SemanticModel.GetConstantValue(indexer.ArgumentList.Arguments.First().Expression); - var positionValue = (int)position.Value; - if (position.HasValue && positionValue > parentCallInfo.Parameters.Length - 1) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.CallInfoArgumentOutOfRange, - indexer.GetLocation(), - positionValue); - - syntaxNodeContext.ReportDiagnostic(diagnostic); - continue; - } - - if (verifyIndexerCast && indexer.Parent is CastExpressionSyntax castExpressionSyntax) - { - var typeInfo = syntaxNodeContext.SemanticModel.GetTypeInfo(castExpressionSyntax.Type); - if (typeInfo.Type != null && !Equals(typeInfo.Type, parentCallInfo.Parameters[positionValue].Type)) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.CallInfoCouldNotConvertParameterAtPosition, - indexer.GetLocation(), - positionValue, - typeInfo.Type); - syntaxNodeContext.ReportDiagnostic(diagnostic); - continue; - } - } - - if (verifyIndexerCast && indexer.Parent is BinaryExpressionSyntax binaryExpressionSyntax && binaryExpressionSyntax.OperatorToken.Kind() == SyntaxKind.AsKeyword) - { - var typeInfo = syntaxNodeContext.SemanticModel.GetTypeInfo(binaryExpressionSyntax.Right); - if (typeInfo.Type != null && !Equals(typeInfo.Type, parentCallInfo.Parameters[positionValue].Type)) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.CallInfoCouldNotConvertParameterAtPosition, - indexer.GetLocation(), - positionValue, - typeInfo.Type); - syntaxNodeContext.ReportDiagnostic(diagnostic); - continue; - } - } - - if (verifyAssignment && indexer.Parent is AssignmentExpressionSyntax assignmentExpressionSyntax && position.HasValue && positionValue < parentCallInfo.Parameters.Length) - { - var parameterSymbol = parentCallInfo.Parameters[positionValue]; - if (parameterSymbol.RefKind != RefKind.Out && parameterSymbol.RefKind != RefKind.Ref) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.CallInfoArgumentIsNotOutOrRef, - indexer.GetLocation(), - positionValue, - parameterSymbol.Type); - syntaxNodeContext.ReportDiagnostic(diagnostic); - continue; - } + return invocationExpressionSyntax.ArgumentList.Arguments.Select(arg => arg.Expression); + } - var typeInfo = syntaxNodeContext.SemanticModel.GetTypeInfo(assignmentExpressionSyntax.Right); - if (typeInfo.Type != null && !Equals(typeInfo.Type, parentCallInfo.Parameters[positionValue].Type)) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.CallInfoArgumentSetWithIncompatibleValue, - indexer.GetLocation(), - typeInfo.Type, - positionValue, - parentCallInfo.Parameters[positionValue].Type); - syntaxNodeContext.ReportDiagnostic(diagnostic); - continue; - } - } - } - } + protected override AbstractCallInfoFinder GetCallInfoFinder() + { + return new CallInfoCallFinder(); } - private bool SupportsCallInfo(SyntaxNodeAnalysisContext syntaxNodeContext, InvocationExpressionSyntax syntax, IMethodSymbol methodSymbol) + protected override SyntaxNode GetSafeCastTypeExpression(ElementAccessExpressionSyntax indexerExpressionSyntax) { - var allArguments = syntax.ArgumentList.Arguments; - var argumentsForAnalysis = methodSymbol.MethodKind == MethodKind.ReducedExtension - ? allArguments - : allArguments.Skip(1); - if (MethodNames.Contains(methodSymbol.Name) == false) + if (indexerExpressionSyntax.Parent is BinaryExpressionSyntax binaryExpressionSyntax && binaryExpressionSyntax.OperatorToken.Kind() == SyntaxKind.AsKeyword) { - return false; + return binaryExpressionSyntax.Right; } - var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(syntax); - - var supportsCallInfo = - symbol.Symbol?.ContainingAssembly?.Name.Equals(MetadataNames.NSubstituteAssemblyName, StringComparison.OrdinalIgnoreCase) == true && - symbol.Symbol?.ContainingType?.ToString().Equals(MetadataNames.NSubstituteSubstituteExtensionsFullTypeName, StringComparison.OrdinalIgnoreCase) == true; - - return supportsCallInfo && IsCalledViaDelegate(syntaxNodeContext.SemanticModel, syntaxNodeContext.SemanticModel.GetTypeInfo(argumentsForAnalysis.First().Expression)); + return null; } - private static bool IsCalledViaDelegate(SemanticModel semanticModel, TypeInfo typeInfo) + protected override SyntaxNode GetUnsafeCastTypeExpression(ElementAccessExpressionSyntax indexerExpressionSyntax) { - var typeSymbol = typeInfo.Type ?? typeInfo.ConvertedType; - var isCalledViaDelegate = typeSymbol != null && - typeSymbol.TypeKind == TypeKind.Delegate && - typeSymbol is INamedTypeSymbol namedTypeSymbol && - namedTypeSymbol.ConstructedFrom.Equals( - semanticModel.Compilation.GetTypeByMetadataName("System.Func`2")) && - IsCallInfoParameter(namedTypeSymbol.TypeArguments.First()); - - return isCalledViaDelegate; - } + if (indexerExpressionSyntax.Parent is CastExpressionSyntax castExpressionSyntax) + { + return castExpressionSyntax.Type; + } - private static bool IsCallInfoParameter(ITypeSymbol symbol) - { - return symbol.ContainingAssembly?.Name.Equals(MetadataNames.NSubstituteAssemblyName, StringComparison.OrdinalIgnoreCase) == true && - symbol.ToString().Equals(MetadataNames.NSubstituteCoreFullTypeName, StringComparison.OrdinalIgnoreCase) == true; + return null; } - } - internal class CallInfoCallFinder - { - public CallInfoContext GetCallInfoContext(SemanticModel semanticModel, SyntaxNode syntaxNode) + protected override SyntaxNode GetAssignmentExpression(ElementAccessExpressionSyntax indexerExpressionSyntax) { - var visitor = new CallInfoVisitor(semanticModel); - visitor.Visit(syntaxNode); + if (indexerExpressionSyntax.Parent is AssignmentExpressionSyntax assignmentExpressionSyntax) + { + return assignmentExpressionSyntax.Right; + } - return new CallInfoContext(visitor.ArgAtInvocations, visitor.ArgInvocations, visitor.DirectIndexerAccesses); + return null; } - } - internal class CallInfoVisitor : CSharpSyntaxWalker - { - private readonly SemanticModel _semanticModel; - - public List ArgAtInvocations { get; } - - public List ArgInvocations { get; } - - public List DirectIndexerAccesses { get; } - - public CallInfoVisitor(SemanticModel semanticModel) + protected override ISymbol GetIndexerSymbol(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, ElementAccessExpressionSyntax indexerExpressionSyntax) { - _semanticModel = semanticModel; - DirectIndexerAccesses = new List(); - ArgAtInvocations = new List(); - ArgInvocations = new List(); + var info = syntaxNodeAnalysisContext.SemanticModel.GetSymbolInfo(indexerExpressionSyntax.Parent.DescendantNodes().First()); + return info.Symbol; } - public override void VisitInvocationExpression(InvocationExpressionSyntax node) + protected override int? GetArgAtPosition(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, InvocationExpressionSyntax invocationExpressionSyntax) { - var symbolInfo = _semanticModel.GetSymbolInfo(node); - - if (symbolInfo.Symbol != null && - symbolInfo.Symbol.ContainingType.ToString().Equals(MetadataNames.NSubstituteCoreFullTypeName)) - { - if (symbolInfo.Symbol.Name == MetadataNames.CallInfoArgAtMethod) - { - ArgAtInvocations.Add(node); - } - - if (symbolInfo.Symbol.Name == MetadataNames.CallInfoArgMethod) - { - ArgInvocations.Add(node); - } - } - - base.VisitInvocationExpression(node); + var position = syntaxNodeAnalysisContext.SemanticModel.GetConstantValue(invocationExpressionSyntax.ArgumentList.Arguments.First().Expression); + return (int?)(position.HasValue ? position.Value : null); } - public override void VisitElementAccessExpression(ElementAccessExpressionSyntax node) + protected override int? GetIndexerPosition(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, ElementAccessExpressionSyntax indexerExpressionSyntax) { - var symbolInfo = _semanticModel.GetSymbolInfo(node).Symbol ?? _semanticModel.GetSymbolInfo(node.Expression).Symbol; - if (symbolInfo != null && symbolInfo.ContainingType.ToString().Equals(MetadataNames.NSubstituteCoreFullTypeName)) - { - DirectIndexerAccesses.Add(node); - } - - base.VisitElementAccessExpression(node); + var position = syntaxNodeAnalysisContext.SemanticModel.GetConstantValue(indexerExpressionSyntax.ArgumentList.Arguments.First().Expression); + return (int?)(position.HasValue ? position.Value : null); } } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoCallFinder.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoCallFinder.cs new file mode 100644 index 00000000..9bfb2737 --- /dev/null +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoCallFinder.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers; + +namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers +{ + internal class CallInfoCallFinder : AbstractCallInfoFinder + { + public override CallInfoContext GetCallInfoContext(SemanticModel semanticModel, SyntaxNode syntaxNode) + { + var visitor = new CallInfoVisitor(semanticModel); + visitor.Visit(syntaxNode); + + return new CallInfoContext(visitor.ArgAtInvocations, visitor.ArgInvocations, visitor.DirectIndexerAccesses); + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoContext.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoContext.cs deleted file mode 100644 index f2881c75..00000000 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoContext.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers -{ - internal class CallInfoContext - { - public List IndexerAccesses { get; } - - public List ArgAtInvocations { get; } - - public List ArgInvocations { get; } - - public CallInfoContext(List argAtInvocations, List argInvocations, List indexerAccesses) - { - IndexerAccesses = indexerAccesses; - ArgAtInvocations = argAtInvocations; - ArgInvocations = argInvocations; - } - } -} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoVisitor.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoVisitor.cs new file mode 100644 index 00000000..50b12c85 --- /dev/null +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoVisitor.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NSubstitute.Analyzers.Shared; + +namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers +{ + internal class CallInfoVisitor : CSharpSyntaxWalker + { + private readonly SemanticModel _semanticModel; + + public List ArgAtInvocations { get; } + + public List ArgInvocations { get; } + + public List DirectIndexerAccesses { get; } + + public CallInfoVisitor(SemanticModel semanticModel) + { + _semanticModel = semanticModel; + DirectIndexerAccesses = new List(); + ArgAtInvocations = new List(); + ArgInvocations = new List(); + } + + public override void VisitInvocationExpression(InvocationExpressionSyntax node) + { + var symbolInfo = _semanticModel.GetSymbolInfo(node); + + if (symbolInfo.Symbol != null && + symbolInfo.Symbol.ContainingType.ToString().Equals(MetadataNames.NSubstituteCoreFullTypeName)) + { + if (symbolInfo.Symbol.Name == MetadataNames.CallInfoArgAtMethod) + { + ArgAtInvocations.Add(node); + } + + if (symbolInfo.Symbol.Name == MetadataNames.CallInfoArgMethod) + { + ArgInvocations.Add(node); + } + } + + base.VisitInvocationExpression(node); + } + + public override void VisitElementAccessExpression(ElementAccessExpressionSyntax node) + { + var symbolInfo = _semanticModel.GetSymbolInfo(node).Symbol ?? _semanticModel.GetSymbolInfo(node.Expression).Symbol; + if (symbolInfo != null && symbolInfo.ContainingType.ToString().Equals(MetadataNames.NSubstituteCoreFullTypeName)) + { + DirectIndexerAccesses.Add(node); + } + + base.VisitElementAccessExpression(node); + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoAnalyzer.cs new file mode 100644 index 00000000..414ff0e6 --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoAnalyzer.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal abstract class AbstractCallInfoAnalyzer : AbstractDiagnosticAnalyzer + where TInvocationExpressionSyntax : SyntaxNode + where TExpressionSyntax : SyntaxNode + where TIndexerExpressionSyntax : SyntaxNode + where TSyntaxKind : struct + { + protected AbstractCallInfoAnalyzer(IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider) + : base(diagnosticDescriptorsProvider) + { + } + + private static readonly ImmutableHashSet MethodNames = ImmutableHashSet.Create( + MetadataNames.NSubstituteReturnsMethod, + MetadataNames.NSubstituteReturnsForAnyArgsMethod); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + DiagnosticDescriptorsProvider.CallInfoArgumentOutOfRange, + DiagnosticDescriptorsProvider.CallInfoCouldNotConvertParameterAtPosition, + DiagnosticDescriptorsProvider.CallInfoCouldNotFindArgumentToThisCall, + DiagnosticDescriptorsProvider.CallInfoMoreThanOneArgumentOfType, + DiagnosticDescriptorsProvider.CallInfoArgumentSetWithIncompatibleValue, + DiagnosticDescriptorsProvider.CallInfoArgumentIsNotOutOrRef); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeInvocation, InvocationExpressionKind); + } + + protected abstract TSyntaxKind InvocationExpressionKind { get; } + + protected abstract SyntaxNode GetParentMethodCall(TInvocationExpressionSyntax invocationExpressionSyntax); + + protected abstract IEnumerable GetArgumentExpressions(TInvocationExpressionSyntax invocationExpressionSyntax); + + protected abstract AbstractCallInfoFinder GetCallInfoFinder(); + + protected abstract SyntaxNode GetSafeCastTypeExpression(TIndexerExpressionSyntax indexerExpressionSyntax); + + protected abstract SyntaxNode GetUnsafeCastTypeExpression(TIndexerExpressionSyntax indexerExpressionSyntax); + + protected abstract SyntaxNode GetAssignmentExpression(TIndexerExpressionSyntax indexerExpressionSyntax); + + protected abstract ISymbol GetIndexerSymbol(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, TIndexerExpressionSyntax indexerExpressionSyntax); + + protected abstract int? GetArgAtPosition(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, TInvocationExpressionSyntax invocationExpressionSyntax); + + protected abstract int? GetIndexerPosition(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, TIndexerExpressionSyntax indexerExpressionSyntax); + + private IndexerInfo GetIndexerInfo(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, TIndexerExpressionSyntax indexerExpressionSyntax) + { + var info = GetIndexerSymbol(syntaxNodeAnalysisContext, indexerExpressionSyntax); + var symbol = info as IMethodSymbol; + var verifyIndexerCast = symbol == null || symbol.Name != MetadataNames.CallInfoArgTypesMethod; + var verifyAssignment = symbol == null; + + var indexerInfo = new IndexerInfo(verifyIndexerCast, verifyAssignment); + return indexerInfo; + } + + 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 (SupportsCallInfo(syntaxNodeContext, invocationExpression, methodSymbol) == false) + { + return; + } + + var parentMethodCallSyntax = GetParentMethodCall(invocationExpression); + var parentCallInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(parentMethodCallSyntax).Symbol as IMethodSymbol; + if (parentCallInfo == null) + { + return; + } + + foreach (var argumentExpressionSyntax in GetArgumentExpressions(invocationExpression)) + { + var finder = GetCallInfoFinder(); + var callInfoContext = finder.GetCallInfoContext(syntaxNodeContext.SemanticModel, argumentExpressionSyntax); + foreach (var argAtInvocation in callInfoContext.ArgAtInvocations) + { + var position = GetArgAtPosition(syntaxNodeContext, argAtInvocation); + if (position.HasValue) + { + if (position.Value > parentCallInfo.Parameters.Length - 1) + { + var diagnostic = Diagnostic.Create( + DiagnosticDescriptorsProvider.CallInfoArgumentOutOfRange, + argAtInvocation.GetLocation(), + position); + + syntaxNodeContext.ReportDiagnostic(diagnostic); + continue; + } + + var symbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(argAtInvocation); + if (symbolInfo.Symbol != null && + symbolInfo.Symbol is IMethodSymbol argAtMethodSymbol && + Equals(parentCallInfo.Parameters[position.Value].Type, argAtMethodSymbol.TypeArguments.First()) == false) + { + var diagnostic = Diagnostic.Create( + DiagnosticDescriptorsProvider.CallInfoCouldNotConvertParameterAtPosition, + argAtInvocation.GetLocation(), + position, + argAtMethodSymbol.TypeArguments.First()); + + syntaxNodeContext.ReportDiagnostic(diagnostic); + } + } + } + + foreach (var argInvocation in callInfoContext.ArgInvocations) + { + var symbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(argInvocation); + if (symbolInfo.Symbol != null && symbolInfo.Symbol is IMethodSymbol argMethodSymbol) + { + var typeSymbol = argMethodSymbol.TypeArguments.First(); + var parameterCount = parentCallInfo.Parameters.Count(param => Equals(param.Type, typeSymbol)); + if (parameterCount == 0) + { + var diagnostic = Diagnostic.Create( + DiagnosticDescriptorsProvider.CallInfoCouldNotFindArgumentToThisCall, + argInvocation.GetLocation(), + typeSymbol); + + syntaxNodeContext.ReportDiagnostic(diagnostic); + continue; + } + + if (parameterCount > 1) + { + var diagnostic = Diagnostic.Create( + DiagnosticDescriptorsProvider.CallInfoMoreThanOneArgumentOfType, + argInvocation.GetLocation(), + typeSymbol); + + syntaxNodeContext.ReportDiagnostic(diagnostic); + } + } + } + + foreach (var indexer in callInfoContext.IndexerAccesses) + { + var indexerInfo = GetIndexerInfo(syntaxNodeContext, indexer); + + var position = GetIndexerPosition(syntaxNodeContext, indexer); + if (position.HasValue && position.Value > parentCallInfo.Parameters.Length - 1) + { + var diagnostic = Diagnostic.Create( + DiagnosticDescriptorsProvider.CallInfoArgumentOutOfRange, + indexer.GetLocation(), + position.Value); + + syntaxNodeContext.ReportDiagnostic(diagnostic); + continue; + } + + var safeCastTypeExpression = GetSafeCastTypeExpression(indexer); + if (indexerInfo.VerifyIndexerCast && safeCastTypeExpression != null) + { + var typeInfo = syntaxNodeContext.SemanticModel.GetTypeInfo(safeCastTypeExpression); + if (typeInfo.Type != null && !Equals(typeInfo.Type, parentCallInfo.Parameters[position.Value].Type)) + { + var diagnostic = Diagnostic.Create( + DiagnosticDescriptorsProvider.CallInfoCouldNotConvertParameterAtPosition, + indexer.GetLocation(), + position.Value, + typeInfo.Type); + syntaxNodeContext.ReportDiagnostic(diagnostic); + continue; + } + } + + var unsafeExpressionType = GetUnsafeCastTypeExpression(indexer); + if (indexerInfo.VerifyIndexerCast && unsafeExpressionType != null) + { + var typeInfo = syntaxNodeContext.SemanticModel.GetTypeInfo(unsafeExpressionType); + if (typeInfo.Type != null && !Equals(typeInfo.Type, parentCallInfo.Parameters[position.Value].Type)) + { + var diagnostic = Diagnostic.Create( + DiagnosticDescriptorsProvider.CallInfoCouldNotConvertParameterAtPosition, + indexer.GetLocation(), + position.Value, + typeInfo.Type); + syntaxNodeContext.ReportDiagnostic(diagnostic); + continue; + } + } + + var assignmentExpressionSyntax = GetAssignmentExpression(indexer); + if (indexerInfo.VerifyAssignment && assignmentExpressionSyntax != null && position.HasValue && position.Value < parentCallInfo.Parameters.Length) + { + var parameterSymbol = parentCallInfo.Parameters[position.Value]; + if (parameterSymbol.RefKind != RefKind.Out && parameterSymbol.RefKind != RefKind.Ref) + { + var diagnostic = Diagnostic.Create( + DiagnosticDescriptorsProvider.CallInfoArgumentIsNotOutOrRef, + indexer.GetLocation(), + position.Value, + parameterSymbol.Type); + syntaxNodeContext.ReportDiagnostic(diagnostic); + continue; + } + + var typeInfo = syntaxNodeContext.SemanticModel.GetTypeInfo(assignmentExpressionSyntax); + if (typeInfo.Type != null && !Equals(typeInfo.Type, parentCallInfo.Parameters[position.Value].Type)) + { + var diagnostic = Diagnostic.Create( + DiagnosticDescriptorsProvider.CallInfoArgumentSetWithIncompatibleValue, + indexer.GetLocation(), + typeInfo.Type, + position.Value, + parentCallInfo.Parameters[position.Value].Type); + syntaxNodeContext.ReportDiagnostic(diagnostic); + continue; + } + } + } + } + } + + private bool SupportsCallInfo(SyntaxNodeAnalysisContext syntaxNodeContext, TInvocationExpressionSyntax syntax, IMethodSymbol methodSymbol) + { + var allArguments = GetArgumentExpressions(syntax); + var argumentsForAnalysis = methodSymbol.MethodKind == MethodKind.ReducedExtension + ? allArguments + : allArguments.Skip(1); + if (MethodNames.Contains(methodSymbol.Name) == false) + { + return false; + } + + var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(syntax); + + var supportsCallInfo = + symbol.Symbol?.ContainingAssembly?.Name.Equals(MetadataNames.NSubstituteAssemblyName, StringComparison.OrdinalIgnoreCase) == true && + symbol.Symbol?.ContainingType?.ToString().Equals(MetadataNames.NSubstituteSubstituteExtensionsFullTypeName, StringComparison.OrdinalIgnoreCase) == true; + + return supportsCallInfo && IsCalledViaDelegate(syntaxNodeContext.SemanticModel, syntaxNodeContext.SemanticModel.GetTypeInfo(argumentsForAnalysis.First())); + } + + private static bool IsCalledViaDelegate(SemanticModel semanticModel, TypeInfo typeInfo) + { + var typeSymbol = typeInfo.Type ?? typeInfo.ConvertedType; + var isCalledViaDelegate = typeSymbol != null && + typeSymbol.TypeKind == TypeKind.Delegate && + typeSymbol is INamedTypeSymbol namedTypeSymbol && + namedTypeSymbol.ConstructedFrom.Equals( + semanticModel.Compilation.GetTypeByMetadataName("System.Func`2")) && + IsCallInfoParameter(namedTypeSymbol.TypeArguments.First()); + + return isCalledViaDelegate; + } + + private static bool IsCallInfoParameter(ITypeSymbol symbol) + { + return symbol.ContainingAssembly?.Name.Equals(MetadataNames.NSubstituteAssemblyName, StringComparison.OrdinalIgnoreCase) == true && + symbol.ToString().Equals(MetadataNames.NSubstituteCoreFullTypeName, StringComparison.OrdinalIgnoreCase) == true; + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoFinder.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoFinder.cs new file mode 100644 index 00000000..28e0f20e --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoFinder.cs @@ -0,0 +1,9 @@ +using Microsoft.CodeAnalysis; + +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal abstract class AbstractCallInfoFinder + { + public abstract CallInfoContext GetCallInfoContext(SemanticModel semanticModel, SyntaxNode syntaxNode); + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/CallInfoContext.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/CallInfoContext.cs new file mode 100644 index 00000000..5ff646d6 --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/CallInfoContext.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal class CallInfoContext + { + public List IndexerAccesses { get; } + + public List ArgAtInvocations { get; } + + public List ArgInvocations { get; } + + public CallInfoContext(List argAtInvocations, List argInvocations, List indexerAccesses) + { + IndexerAccesses = indexerAccesses; + ArgAtInvocations = argAtInvocations; + ArgInvocations = argInvocations; + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/IndexerInfo.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/IndexerInfo.cs new file mode 100644 index 00000000..52c8937d --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/IndexerInfo.cs @@ -0,0 +1,15 @@ +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal struct IndexerInfo + { + public bool VerifyIndexerCast { get; } + + public bool VerifyAssignment { get; } + + public IndexerInfo(bool verifyIndexerCast, bool verifyAssignment) + { + VerifyIndexerCast = verifyIndexerCast; + VerifyAssignment = verifyAssignment; + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/NSubstitute.Analyzers.Shared.csproj b/src/NSubstitute.Analyzers.Shared/NSubstitute.Analyzers.Shared.csproj index ef32e147..850e2dcf 100644 --- a/src/NSubstitute.Analyzers.Shared/NSubstitute.Analyzers.Shared.csproj +++ b/src/NSubstitute.Analyzers.Shared/NSubstitute.Analyzers.Shared.csproj @@ -7,6 +7,7 @@ True false $(VersionSuffix) + 7.2 diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/ReturnsAsExtensionMethodsTests.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/ReturnsAsExtensionMethodsTests.cs index 55a82e40..f35ccebe 100644 --- a/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/ReturnsAsExtensionMethodsTests.cs +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/ReturnsAsExtensionMethodsTests.cs @@ -146,12 +146,81 @@ public void Test() [InlineData("var x = callInfo[0] as Bar;")] [InlineData("var x = (Bar)callInfo.Args()[0];")] [InlineData("var x = callInfo.Args()[0] as Bar;")] - [InlineData("var x = callInfo.ArgTypes()[0] as object;")] public async Task ReportsNoDiagnostic_WhenConvertingTypeToSupportedType(string argAccess) { var source = $@"using System; using NSubstitute; +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(Bar x); + }} + + public class Bar + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var substitute = NSubstitute.Substitute.For(); + substitute.Bar(Arg.Any()).Returns(callInfo => + {{ + {argAccess} + return 1; + }}); + }} + }} +}}"; + await VerifyDiagnostic(source); + } + + [Theory] + [InlineData("var x = callInfo.ArgTypes() as object;")] + [InlineData("var x = (object)callInfo.ArgTypes();")] + public async Task ReportsNoDiagnostic_WhenCastingElementsFromArgTypes(string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(Bar x); + }} + + public class Bar + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var substitute = NSubstitute.Substitute.For(); + substitute.Bar(Arg.Any()).Returns(callInfo => + {{ + {argAccess} + return 1; + }}); + }} + }} +}}"; + await VerifyDiagnostic(source); + } + + [Theory] + [InlineData("callInfo.ArgTypes()[0] = typeof(object);")] + [InlineData("callInfo.Args()[0] = 1m;")] + public async Task ReportsNoDiagnostic_WhenAssigningValueToNotRefNorOutArgumentViaIndirectCall(string argAccess) + { + var source = $@"using System; +using NSubstitute; + namespace MyNamespace {{ public interface Foo