using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace StellaOps.AirGap.Policy.Analyzers; /// /// Offers a remediation template that routes HttpClient creation through the shared EgressPolicy factory. /// [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(HttpClientUsageCodeFixProvider))] [Shared] public sealed class HttpClientUsageCodeFixProvider : CodeFixProvider { private const string Title = "Use EgressHttpClientFactory.Create(...)"; /// public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(HttpClientUsageAnalyzer.DiagnosticId); /// public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; /// public override async Task RegisterCodeFixesAsync(CodeFixContext context) { if (context.Document is null) { return; } var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); if (root is null) { return; } var diagnostic = context.Diagnostics[0]; var node = root.FindNode(diagnostic.Location.SourceSpan); if (node is not ObjectCreationExpressionSyntax objectCreation) { return; } context.RegisterCodeFix( CodeAction.Create( Title, cancellationToken => ReplaceWithFactoryCallAsync(context.Document, objectCreation, cancellationToken), equivalenceKey: Title), diagnostic); } private static async Task ReplaceWithFactoryCallAsync(Document document, ObjectCreationExpressionSyntax creation, CancellationToken cancellationToken) { var replacementExpression = BuildReplacementExpression(creation); var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (root is null) { return document; } var updatedRoot = root.ReplaceNode(creation, replacementExpression.WithTriviaFrom(creation)); return document.WithSyntaxRoot(updatedRoot); } private static ExpressionSyntax BuildReplacementExpression(ObjectCreationExpressionSyntax creation) { var requestExpression = SyntaxFactory.ParseExpression( "new global::StellaOps.AirGap.Policy.EgressRequest(" + "component: \"REPLACE_COMPONENT\", " + "destination: new global::System.Uri(\"https://replace-with-endpoint\"), " + "intent: \"REPLACE_INTENT\")"); var egressPolicyExpression = SyntaxFactory.ParseExpression( "default(global::StellaOps.AirGap.Policy.IEgressPolicy)"); var arguments = new List { SyntaxFactory.Argument(egressPolicyExpression) .WithNameColon(SyntaxFactory.NameColon("egressPolicy")) .WithTrailingTrivia( SyntaxFactory.Space, SyntaxFactory.Comment("/* TODO: provide IEgressPolicy instance */")), SyntaxFactory.Argument(requestExpression) .WithNameColon(SyntaxFactory.NameColon("request")) }; if (ShouldUseClientFactory(creation)) { var clientFactoryLambda = SyntaxFactory.ParenthesizedLambdaExpression( SyntaxFactory.ParameterList(), CreateHttpClientExpression(creation)); arguments.Add( SyntaxFactory.Argument(clientFactoryLambda) .WithNameColon(SyntaxFactory.NameColon("clientFactory"))); } return SyntaxFactory.InvocationExpression( SyntaxFactory.ParseExpression("global::StellaOps.AirGap.Policy.EgressHttpClientFactory.Create")) .WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments))); } private static bool ShouldUseClientFactory(ObjectCreationExpressionSyntax creation) => (creation.ArgumentList?.Arguments.Count ?? 0) > 0 || creation.Initializer is not null; private static ObjectCreationExpressionSyntax CreateHttpClientExpression(ObjectCreationExpressionSyntax creation) { var httpClientType = SyntaxFactory.ParseTypeName("global::System.Net.Http.HttpClient"); var arguments = creation.ArgumentList ?? SyntaxFactory.ArgumentList(); return SyntaxFactory.ObjectCreationExpression(httpClientType) .WithArgumentList(arguments) .WithInitializer(creation.Initializer); } }