Skip to content

Commit 800196a

Browse files
authored
Add code fix for FunctionContext helper migration (#79)
* Add FunctionContext helper code fix * Clarify FunctionContext analyzer docs * Tighten FunctionContext migration fixes
1 parent 3396022 commit 800196a

10 files changed

Lines changed: 468 additions & 15 deletions

File tree

FastMoq.Analyzers.Tests/AnalyzerTestHelpers.cs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ internal static class AnalyzerTestHelpers
1818
{
1919
public static async Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(string source, params DiagnosticAnalyzer[] analyzers)
2020
{
21-
var document = CreateDocument(source);
21+
return await GetDiagnosticsAsync(source, includeAzureFunctionsHelpers: false, analyzers).ConfigureAwait(false);
22+
}
23+
24+
public static async Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(string source, bool includeAzureFunctionsHelpers, params DiagnosticAnalyzer[] analyzers)
25+
{
26+
var document = CreateDocument(source, includeAzureFunctionsHelpers);
2227
return await GetDiagnosticsAsync(document, analyzers).ConfigureAwait(false);
2328
}
2429

@@ -36,9 +41,9 @@ public static async Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(Documen
3641
.ConfigureAwait(false);
3742
}
3843

39-
public static async Task<string> ApplyCodeFixAsync(string source, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string diagnosticId)
44+
public static async Task<string> ApplyCodeFixAsync(string source, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string diagnosticId, bool includeAzureFunctionsHelpers = false)
4045
{
41-
var document = CreateDocument(source);
46+
var document = CreateDocument(source, includeAzureFunctionsHelpers);
4247
var diagnostics = await GetDiagnosticsAsync(document, analyzer).ConfigureAwait(false);
4348
var diagnostic = diagnostics.Single(item => item.Id == diagnosticId);
4449

@@ -54,6 +59,19 @@ public static async Task<string> ApplyCodeFixAsync(string source, DiagnosticAnal
5459
return changedRoot!.NormalizeWhitespace().ToFullString();
5560
}
5661

62+
public static async Task<ImmutableArray<string>> GetCodeFixTitlesAsync(string source, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string diagnosticId, bool includeAzureFunctionsHelpers = false)
63+
{
64+
var document = CreateDocument(source, includeAzureFunctionsHelpers);
65+
var diagnostics = await GetDiagnosticsAsync(document, analyzer).ConfigureAwait(false);
66+
var diagnostic = diagnostics.Single(item => item.Id == diagnosticId);
67+
68+
var actions = new List<CodeAction>();
69+
var context = new CodeFixContext(document, diagnostic, (action, _) => actions.Add(action), CancellationToken.None);
70+
await codeFixProvider.RegisterCodeFixesAsync(context).ConfigureAwait(false);
71+
72+
return actions.Select(action => action.Title).ToImmutableArray();
73+
}
74+
5775
public static string NormalizeCode(string source)
5876
{
5977
return CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Preview))
@@ -62,7 +80,7 @@ public static string NormalizeCode(string source)
6280
.ToFullString();
6381
}
6482

65-
private static Document CreateDocument(string source)
83+
private static Document CreateDocument(string source, bool includeAzureFunctionsHelpers = false)
6684
{
6785
var workspace = new AdhocWorkspace();
6886
var projectId = ProjectId.CreateNewId();
@@ -73,7 +91,7 @@ private static Document CreateDocument(string source)
7391
.WithProjectParseOptions(projectId, new CSharpParseOptions(LanguageVersion.Preview))
7492
.WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
7593

76-
foreach (var metadataReference in GetMetadataReferences())
94+
foreach (var metadataReference in GetMetadataReferences(includeAzureFunctionsHelpers))
7795
{
7896
solution = solution.AddMetadataReference(projectId, metadataReference);
7997
}
@@ -82,13 +100,19 @@ private static Document CreateDocument(string source)
82100
return solution.GetDocument(documentId)!;
83101
}
84102

85-
private static IEnumerable<MetadataReference> GetMetadataReferences()
103+
private static IEnumerable<MetadataReference> GetMetadataReferences(bool includeAzureFunctionsHelpers)
86104
{
87105
var references = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
88106
if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string trustedPlatformAssemblies)
89107
{
90108
foreach (var assemblyPath in trustedPlatformAssemblies.Split(Path.PathSeparator))
91109
{
110+
if (!includeAzureFunctionsHelpers &&
111+
string.Equals(Path.GetFileNameWithoutExtension(assemblyPath), "FastMoq.AzureFunctions", StringComparison.OrdinalIgnoreCase))
112+
{
113+
continue;
114+
}
115+
92116
references.Add(assemblyPath);
93117
}
94118
}
@@ -102,6 +126,13 @@ private static IEnumerable<MetadataReference> GetMetadataReferences()
102126
references.Add(typeof(Microsoft.AspNetCore.Http.DefaultHttpContext).Assembly.Location);
103127
references.Add(typeof(Microsoft.AspNetCore.Mvc.ControllerContext).Assembly.Location);
104128

129+
if (includeAzureFunctionsHelpers)
130+
{
131+
references.Add(typeof(FastMoq.AzureFunctions.Extensions.FunctionContextTestExtensions).Assembly.Location);
132+
references.Add(typeof(Microsoft.Azure.Functions.Worker.FunctionContext).Assembly.Location);
133+
references.Add(typeof(Azure.Core.Serialization.ObjectSerializer).Assembly.Location);
134+
}
135+
105136
return references.Select(path => MetadataReference.CreateFromFile(path));
106137
}
107138
}

FastMoq.Analyzers.Tests/FastMoq.Analyzers.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
<ItemGroup>
3333
<ProjectReference Include="..\FastMoq.Analyzers\FastMoq.Analyzers.csproj" OutputItemType="Analyzer" />
34+
<ProjectReference Include="..\FastMoq.AzureFunctions\FastMoq.AzureFunctions.csproj" />
3435
<ProjectReference Include="..\FastMoq.Core\FastMoq.Core.csproj" />
3536
<ProjectReference Include="..\FastMoq.Provider.Moq\FastMoq.Provider.Moq.csproj" />
3637
<ProjectReference Include="..\FastMoq.Provider.NSubstitute\FastMoq.Provider.NSubstitute.csproj" />

FastMoq.Analyzers.Tests/MigrationAnalyzerTests.cs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,166 @@ void Execute(Mocker Mocks)
651651
Assert.Equal(DiagnosticIds.PreferTypedServiceProviderHelpers, diagnostic.Id);
652652
}
653653

654+
[Fact]
655+
public async Task ServiceProviderShimAnalyzer_ShouldReportAndFix_FunctionContextInstanceServicesReturnsUsage()
656+
{
657+
const string SOURCE = @"
658+
using System;
659+
using FastMoq;
660+
using FastMoq.Providers.MoqProvider;
661+
662+
namespace Microsoft.Azure.Functions.Worker
663+
{
664+
abstract class FunctionContext
665+
{
666+
public virtual IServiceProvider InstanceServices { get; set; }
667+
}
668+
}
669+
670+
class Sample
671+
{
672+
void Execute(Mocker Mocks, IServiceProvider provider)
673+
{
674+
var context = Mocks.GetOrCreateMock<Microsoft.Azure.Functions.Worker.FunctionContext>();
675+
context.Setup(x => x.InstanceServices).Returns(provider);
676+
}
677+
}";
678+
679+
var diagnostics = await AnalyzerTestHelpers.GetDiagnosticsAsync(SOURCE, new ServiceProviderShimAnalyzer());
680+
var diagnostic = Assert.Single(diagnostics.Where(item => item.Id == DiagnosticIds.PreferTypedServiceProviderHelpers));
681+
Assert.Equal(DiagnosticIds.PreferTypedServiceProviderHelpers, diagnostic.Id);
682+
683+
var fixedSource = await AnalyzerTestHelpers.ApplyCodeFixAsync(
684+
SOURCE,
685+
new ServiceProviderShimAnalyzer(),
686+
codeFixProvider,
687+
DiagnosticIds.PreferTypedServiceProviderHelpers,
688+
includeAzureFunctionsHelpers: true);
689+
var expected = AnalyzerTestHelpers.NormalizeCode(@"
690+
using System;
691+
using FastMoq;
692+
using FastMoq.Providers.MoqProvider;
693+
using FastMoq.AzureFunctions.Extensions;
694+
695+
namespace Microsoft.Azure.Functions.Worker
696+
{
697+
abstract class FunctionContext
698+
{
699+
public virtual IServiceProvider InstanceServices { get; set; }
700+
}
701+
}
702+
703+
class Sample
704+
{
705+
void Execute(Mocker Mocks, IServiceProvider provider)
706+
{
707+
var context = Mocks.GetOrCreateMock<Microsoft.Azure.Functions.Worker.FunctionContext>();
708+
context.AddFunctionContextInstanceServices(provider);
709+
}
710+
}");
711+
712+
Assert.Equal(expected, fixedSource);
713+
}
714+
715+
[Fact]
716+
public async Task ServiceProviderShimAnalyzer_ShouldReportButNotOfferFix_WhenFunctionContextHelperPackageIsUnavailable()
717+
{
718+
const string SOURCE = @"
719+
using System;
720+
using FastMoq;
721+
using FastMoq.Providers.MoqProvider;
722+
723+
namespace Microsoft.Azure.Functions.Worker
724+
{
725+
abstract class FunctionContext
726+
{
727+
public virtual IServiceProvider InstanceServices { get; set; }
728+
}
729+
}
730+
731+
class Sample
732+
{
733+
void Execute(Mocker Mocks, IServiceProvider provider)
734+
{
735+
var context = Mocks.GetOrCreateMock<Microsoft.Azure.Functions.Worker.FunctionContext>();
736+
context.SetupGet(x => x.InstanceServices).Returns(provider);
737+
}
738+
}";
739+
740+
var diagnostics = await AnalyzerTestHelpers.GetDiagnosticsAsync(SOURCE, new ServiceProviderShimAnalyzer());
741+
var diagnostic = Assert.Single(diagnostics.Where(item => item.Id == DiagnosticIds.PreferTypedServiceProviderHelpers));
742+
Assert.Equal(DiagnosticIds.PreferTypedServiceProviderHelpers, diagnostic.Id);
743+
744+
var codeFixTitles = await AnalyzerTestHelpers.GetCodeFixTitlesAsync(
745+
SOURCE,
746+
new ServiceProviderShimAnalyzer(),
747+
codeFixProvider,
748+
DiagnosticIds.PreferTypedServiceProviderHelpers);
749+
750+
Assert.Empty(codeFixTitles);
751+
}
752+
753+
[Fact]
754+
public async Task ServiceProviderShimAnalyzer_ShouldReportAndFix_FunctionContextInstanceServicesSetupPropertyUsage()
755+
{
756+
const string SOURCE = @"
757+
using System;
758+
using FastMoq;
759+
using FastMoq.Providers.MoqProvider;
760+
761+
namespace Microsoft.Azure.Functions.Worker
762+
{
763+
abstract class FunctionContext
764+
{
765+
public virtual IServiceProvider InstanceServices { get; set; }
766+
}
767+
}
768+
769+
class Sample
770+
{
771+
void Execute(Mocker Mocks, IServiceProvider provider)
772+
{
773+
var context = Mocks.GetOrCreateMock<Microsoft.Azure.Functions.Worker.FunctionContext>();
774+
context.AsMoq().SetupProperty(x => x.InstanceServices, provider);
775+
}
776+
}";
777+
778+
var diagnostics = await AnalyzerTestHelpers.GetDiagnosticsAsync(SOURCE, new ServiceProviderShimAnalyzer());
779+
var diagnostic = Assert.Single(diagnostics.Where(item => item.Id == DiagnosticIds.PreferTypedServiceProviderHelpers));
780+
Assert.Equal(DiagnosticIds.PreferTypedServiceProviderHelpers, diagnostic.Id);
781+
782+
var fixedSource = await AnalyzerTestHelpers.ApplyCodeFixAsync(
783+
SOURCE,
784+
new ServiceProviderShimAnalyzer(),
785+
codeFixProvider,
786+
DiagnosticIds.PreferTypedServiceProviderHelpers,
787+
includeAzureFunctionsHelpers: true);
788+
var expected = AnalyzerTestHelpers.NormalizeCode(@"
789+
using System;
790+
using FastMoq;
791+
using FastMoq.Providers.MoqProvider;
792+
using FastMoq.AzureFunctions.Extensions;
793+
794+
namespace Microsoft.Azure.Functions.Worker
795+
{
796+
abstract class FunctionContext
797+
{
798+
public virtual IServiceProvider InstanceServices { get; set; }
799+
}
800+
}
801+
802+
class Sample
803+
{
804+
void Execute(Mocker Mocks, IServiceProvider provider)
805+
{
806+
var context = Mocks.GetOrCreateMock<Microsoft.Azure.Functions.Worker.FunctionContext>();
807+
context.AddFunctionContextInstanceServices(provider);
808+
}
809+
}");
810+
811+
Assert.Equal(expected, fixedSource);
812+
}
813+
654814
[Fact]
655815
public async Task KnownTypeAuthoringAnalyzer_ShouldReport_WhenContextAwareAddTypeOverloadIsUsed()
656816
{

FastMoq.Analyzers/CodeFixes/FastMoqMigrationCodeFixProvider.cs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public sealed class FastMoqMigrationCodeFixProvider : CodeFixProvider
2020
DiagnosticIds.UseVerifyLogged,
2121
DiagnosticIds.UseConsistentMockRetrieval,
2222
DiagnosticIds.UseProviderFirstMockRetrieval,
23+
DiagnosticIds.PreferTypedServiceProviderHelpers,
2324
DiagnosticIds.UseExplicitOptionalParameterResolution,
2425
DiagnosticIds.ReplaceInitializeCompatibilityWrapper,
2526
DiagnosticIds.PreferSetupOptionsHelper);
@@ -107,6 +108,31 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
107108
break;
108109
}
109110

111+
case DiagnosticIds.PreferTypedServiceProviderHelpers:
112+
{
113+
var invocationExpression = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf<InvocationExpressionSyntax>();
114+
if (invocationExpression is null)
115+
{
116+
return;
117+
}
118+
119+
var semanticModel = await document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
120+
if (semanticModel is null ||
121+
!FastMoqAnalysisHelpers.HasFunctionContextInstanceServicesMockHelper(semanticModel) ||
122+
!FastMoqAnalysisHelpers.TryBuildFunctionContextInstanceServicesReplacement(invocationExpression, semanticModel, context.CancellationToken, out _, out _))
123+
{
124+
return;
125+
}
126+
127+
context.RegisterCodeFix(
128+
CodeAction.Create(
129+
"Use AddFunctionContextInstanceServices(...)",
130+
cancellationToken => ReplaceFunctionContextInstanceServicesInvocationAsync(document, invocationExpression, cancellationToken),
131+
nameof(DiagnosticIds.PreferTypedServiceProviderHelpers)),
132+
diagnostic);
133+
break;
134+
}
135+
110136
case DiagnosticIds.UseExplicitOptionalParameterResolution:
111137
{
112138
var assignmentExpression = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf<AssignmentExpressionSyntax>();
@@ -302,11 +328,27 @@ private static async Task<Document> ReplaceSetupOptionsInvocationAsync(Document
302328
.WithTriviaFrom(invocationExpression);
303329
var updatedRoot = root.ReplaceNode(invocationExpression, replacementExpression);
304330

305-
if (updatedRoot is CompilationUnitSyntax compilationUnit && !compilationUnit.Usings.Any(@using => @using.Name?.ToString() == "FastMoq.Extensions"))
331+
return document.WithSyntaxRoot(AddUsingDirectiveIfMissing(updatedRoot, "FastMoq.Extensions"));
332+
}
333+
334+
private static async Task<Document> ReplaceFunctionContextInstanceServicesInvocationAsync(Document document, InvocationExpressionSyntax invocationExpression, CancellationToken cancellationToken)
335+
{
336+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
337+
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
338+
if (root is null || semanticModel is null)
306339
{
307-
updatedRoot = compilationUnit.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("FastMoq.Extensions")));
340+
return document;
308341
}
309342

343+
if (!FastMoqAnalysisHelpers.TryBuildFunctionContextInstanceServicesReplacement(invocationExpression, semanticModel, cancellationToken, out var targetInvocation, out var replacementText))
344+
{
345+
return document;
346+
}
347+
348+
var replacementExpression = SyntaxFactory.ParseExpression(replacementText)
349+
.WithTriviaFrom(targetInvocation);
350+
var updatedRoot = root.ReplaceNode(targetInvocation, replacementExpression);
351+
updatedRoot = AddUsingDirectiveIfMissing(updatedRoot, "FastMoq.AzureFunctions.Extensions");
310352
return document.WithSyntaxRoot(updatedRoot);
311353
}
312354

@@ -326,6 +368,16 @@ private static async Task<Document> ReplaceGetMockAsync(Document document, Membe
326368
return document.WithSyntaxRoot(updatedRoot);
327369
}
328370

371+
private static SyntaxNode AddUsingDirectiveIfMissing(SyntaxNode root, string namespaceName)
372+
{
373+
if (root is CompilationUnitSyntax compilationUnit && !compilationUnit.Usings.Any(@using => @using.Name?.ToString() == namespaceName))
374+
{
375+
return compilationUnit.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(namespaceName)));
376+
}
377+
378+
return root;
379+
}
380+
329381
private delegate string? ReplacementBuilder(Document document, SemanticModel semanticModel, SyntaxNode syntaxNode, CancellationToken cancellationToken);
330382
}
331383
}

0 commit comments

Comments
 (0)