From 9300e008e0e3226661c2622e454cd7a880fb0fcb Mon Sep 17 00:00:00 2001 From: tpodolak Date: Sat, 28 Jul 2018 21:37:12 +0200 Subject: [PATCH] [GH-30] - scaffolding --- .../DiagnosticAnalyzers/CallInfoAnalyzer.cs | 46 ++++++++++++++++ .../AbstractDiagnosticDescriptorsProvider.cs | 2 + .../DiagnosticDescriptors.cs | 8 +++ .../DiagnosticIdentifiers.cs | 1 + .../IDiagnosticDescriptorsProvider.cs | 2 + .../Resources.Designer.cs | 29 +++++++++- .../Resources.resx | 13 ++++- .../CallInfoAnalyzerTests.cs | 54 +++++++++++++++++++ 8 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs create mode 100644 tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/CallInfoAnalyzerTests.cs diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs new file mode 100644 index 00000000..8e4db5d1 --- /dev/null +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs @@ -0,0 +1,46 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers; + +namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class CallInfoAnalyzer : AbstractDiagnosticAnalyzer + { + public CallInfoAnalyzer() + : base(new DiagnosticDescriptorsProvider()) + { + } + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptorsProvider.CallInfoArgumentOutOfRange); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + 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 (IsCallInfoMethod(methodSymbol) == false) + { + return; + } + } + + private bool IsCallInfoMethod(IMethodSymbol methodSymbol) + { + return methodSymbol.ContainingType.ToString() == "NSubstitute.Core.CallInfo"; + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/AbstractDiagnosticDescriptorsProvider.cs b/src/NSubstitute.Analyzers.Shared/AbstractDiagnosticDescriptorsProvider.cs index af8a4821..71fc6525 100644 --- a/src/NSubstitute.Analyzers.Shared/AbstractDiagnosticDescriptorsProvider.cs +++ b/src/NSubstitute.Analyzers.Shared/AbstractDiagnosticDescriptorsProvider.cs @@ -31,5 +31,7 @@ internal class AbstractDiagnosticDescriptorsProvider : IDiagnosticDescriptors public DiagnosticDescriptor NonVirtualWhenSetupSpecification { get; } = DiagnosticDescriptors.NonVirtualWhenSetupSpecification; public DiagnosticDescriptor ReEntrantSubstituteCall { get; } = DiagnosticDescriptors.ReEntrantSubstituteCall; + + public DiagnosticDescriptor CallInfoArgumentOutOfRange { get; } = DiagnosticDescriptors.ReEntrantSubstituteCall; } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs index eeb9ec88..729eb11a 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs @@ -123,6 +123,14 @@ internal class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); + public static DiagnosticDescriptor CallInfoArgumentOutOfRange { get; } = + CreateDiagnosticDescriptor( + name: nameof(CallInfoArgumentOutOfRange), + id: DiagnosticIdentifiers.CallInfoArgumentOutOfRange, + category: DiagnosticCategories.Usage, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + private static DiagnosticDescriptor CreateDiagnosticDescriptor( string name, string id, string category, DiagnosticSeverity defaultSeverity, bool isEnabledByDefault) { diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs index 12f1e596..5f63cd05 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs @@ -15,5 +15,6 @@ internal class DiagnosticIdentifiers public static readonly string NonVirtualReceivedSetupSpecification = "NS011"; public static readonly string NonVirtualWhenSetupSpecification = "NS012"; public static readonly string ReEntrantSubstituteCall = "NS013"; + public static readonly string CallInfoArgumentOutOfRange = "NS014"; } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/IDiagnosticDescriptorsProvider.cs b/src/NSubstitute.Analyzers.Shared/IDiagnosticDescriptorsProvider.cs index 34340140..9975dad9 100644 --- a/src/NSubstitute.Analyzers.Shared/IDiagnosticDescriptorsProvider.cs +++ b/src/NSubstitute.Analyzers.Shared/IDiagnosticDescriptorsProvider.cs @@ -31,5 +31,7 @@ internal interface IDiagnosticDescriptorsProvider DiagnosticDescriptor NonVirtualWhenSetupSpecification { get; } DiagnosticDescriptor ReEntrantSubstituteCall { get; } + + DiagnosticDescriptor CallInfoArgumentOutOfRange { get; } } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/Resources.Designer.cs b/src/NSubstitute.Analyzers.Shared/Resources.Designer.cs index ef7030ab..12c7651d 100644 --- a/src/NSubstitute.Analyzers.Shared/Resources.Designer.cs +++ b/src/NSubstitute.Analyzers.Shared/Resources.Designer.cs @@ -20,7 +20,7 @@ namespace NSubstitute.Analyzers.Shared { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -61,6 +61,33 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to Unable to find argument.. + /// + internal static string CallInfoArgumentOutOfRangeDescription { + get { + return ResourceManager.GetString("CallInfoArgumentOutOfRangeDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There is no argument at position {0}. + /// + internal static string CallInfoArgumentOutOfRangeMessageFormat { + get { + return ResourceManager.GetString("CallInfoArgumentOutOfRangeMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to find matching argument.. + /// + internal static string CallInfoArgumentOutOfRangeTitle { + get { + return ResourceManager.GetString("CallInfoArgumentOutOfRangeTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Non-virtual members can not be intercepted.. /// diff --git a/src/NSubstitute.Analyzers.Shared/Resources.resx b/src/NSubstitute.Analyzers.Shared/Resources.resx index 4b9de721..2471d3d6 100644 --- a/src/NSubstitute.Analyzers.Shared/Resources.resx +++ b/src/NSubstitute.Analyzers.Shared/Resources.resx @@ -230,7 +230,6 @@ Can not provide constructor arguments when substituting for a delegate. An optional longer localizable description of the diagnostic. - Can not provide constructor arguments when substituting for a delegate. The title of the diagnostic. @@ -258,5 +257,17 @@ Re-entrant substitute call. The title of the diagnostic. + + + Unable to find argument. + An optional longer localizable description of the diagnostic. + + + There is no argument at position {0} + The format-able message the diagnostic displays. + + + Unable to find matching argument. + The title of the diagnostic. diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/CallInfoAnalyzerTests.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/CallInfoAnalyzerTests.cs new file mode 100644 index 00000000..974194aa --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/CallInfoAnalyzerTests.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers; +using NSubstitute.Analyzers.Shared; +using NSubstitute.Analyzers.Tests.Shared.DiagnosticAnalyzers; +using Xunit; + +namespace NSubstitute.Analyzers.Tests.CSharp.DiagnosticAnalyzerTests.CallInfoAnalyzerTests +{ + public class CallInfoAnalyzerTests : CSharpDiagnosticVerifier + { + [Fact] + public async Task ReportsDiagnostic_WhenAccessingArgumentOutOfBounds() + { + var source = @"using System; +using NSubstitute; + +namespace MyNamespace +{ + public interface Foo + { + int Bar(int x); + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + substitute.Bar(Arg.Any()).Returns(callInfo => callInfo.ArgAt(1)); + } + } +}"; + var expectedDiagnostic = new DiagnosticResult + { + Id = DiagnosticIdentifiers.CallInfoArgumentOutOfRange, + Severity = DiagnosticSeverity.Warning, + Message = "There is no argument at position 1", + Locations = new[] + { + new DiagnosticResultLocation(18, 13) + } + }; + + await VerifyDiagnostic(source, expectedDiagnostic); + } + + protected override DiagnosticAnalyzer GetDiagnosticAnalyzer() + { + return new CallInfoAnalyzer(); + } + } +} \ No newline at end of file