Skip to content

Commit 443869e

Browse files
committed
Added suggested HttpClient usage analyzer.
1 parent 461e99d commit 443869e

File tree

8 files changed

+272
-7
lines changed

8 files changed

+272
-7
lines changed

release_notes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
- Added warning for customer registering HttpClient with DI (#7674)
77
- Updated Java Worker Version to [1.9.0](https://github.com/Azure/azure-functions-java-worker/releases/tag/1.9.0)
8-
8+
- Added analyzer suggesting best HttpClient usage practices. (#7718)
99

1010
**Release sprint:** Sprint 111
1111
[ [bugs](https://github.com/Azure/azure-functions-host/issues?q=is%3Aissue+milestone%3A%22Functions+Sprint+111%22+label%3Abug+is%3Aclosed) | [features](https://github.com/Azure/azure-functions-host/issues?q=is%3Aissue+milestone%3A%22Functions+Sprint+111%22+label%3Afeature+is%3Aclosed) ]

src/WebJobs.Script.Analyzers/AvoidAsyncVoidAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context)
2424
{
2525
var methodSymbol = (IMethodSymbol)context.Symbol;
2626

27-
if (!methodSymbol.IsFunction(context) || !methodSymbol.IsAsync || !methodSymbol.ReturnsVoid)
27+
if (!methodSymbol.IsFunction(context.Compilation) || !methodSymbol.IsAsync || !methodSymbol.ReturnsVoid)
2828
{
2929
return;
3030
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
using System.Collections.Immutable;
8+
using System.Net.Http;
9+
10+
namespace Microsoft.Azure.Functions.Analyzers
11+
{
12+
/// <summary>
13+
/// AZF0002: Use static HttpClient
14+
///
15+
/// Cause:
16+
/// An local declaration that happens inside a Function method, instantiates an HttpClient
17+
/// </summary>
18+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
19+
public class AvoidNonStaticHttpClientAnalyzer : DiagnosticAnalyzer
20+
{
21+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(DiagnosticDescriptors.AvoidNonStaticHttpClient); } }
22+
23+
public override void Initialize(AnalysisContext context)
24+
{
25+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
26+
context.EnableConcurrentExecution();
27+
28+
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ObjectCreationExpression);
29+
}
30+
31+
private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
32+
{
33+
// check if object is created within function method
34+
var containingSymbol = context.ContainingSymbol as IMethodSymbol;
35+
if (containingSymbol is null || !containingSymbol.IsFunction(context.Compilation))
36+
{
37+
return;
38+
}
39+
40+
// check if constructor is HttpClient
41+
var httpClientTypeSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(HttpClient).FullName);
42+
var nodeTypeSymbol = context.SemanticModel.GetTypeInfo(context.Node, context.CancellationToken).ConvertedType;
43+
44+
if (!httpClientTypeSymbol.IsAssignableFrom(nodeTypeSymbol))
45+
{
46+
return;
47+
}
48+
49+
var diagnostic = Diagnostic.Create(DiagnosticDescriptors.AvoidNonStaticHttpClient, context.Node.GetLocation());
50+
context.ReportDiagnostic(diagnostic);
51+
}
52+
}
53+
}

src/WebJobs.Script.Analyzers/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ internal static class Types
1212

1313
internal static class DiagnosticsCategories
1414
{
15+
public const string Reliability = "Reliability";
1516
public const string Usage = "Usage";
1617
}
1718
}

src/WebJobs.Script.Analyzers/DiagnosticDescriptors.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,11 @@ private static DiagnosticDescriptor Create(string id, string title, string messa
2121
messageFormat: "Async void can lead to unexpected behavior. Return Task instead.",
2222
category: Constants.DiagnosticsCategories.Usage,
2323
severity: DiagnosticSeverity.Error);
24+
25+
public static DiagnosticDescriptor AvoidNonStaticHttpClient { get; }
26+
= Create(id: "AZF0002", title: "Inefficient HttpClient usage",
27+
messageFormat: "Reuse HttpClient instances to avoid holding more connections than necessary. See helplink for more information.",
28+
category: Constants.DiagnosticsCategories.Reliability,
29+
severity: DiagnosticSeverity.Warning);
2430
}
2531
}

src/WebJobs.Script.Analyzers/MethodSymbolExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.Azure.Functions.Analyzers
99
{
1010
internal static class MethodSymbolExtensions
1111
{
12-
public static bool IsFunction(this IMethodSymbol symbol, SymbolAnalysisContext analysisContext)
12+
public static bool IsFunction(this IMethodSymbol symbol, Compilation compilation)
1313
{
1414
var attributes = symbol.GetAttributes();
1515

@@ -18,7 +18,7 @@ public static bool IsFunction(this IMethodSymbol symbol, SymbolAnalysisContext a
1818
return false;
1919
}
2020

21-
var attributeType = analysisContext.Compilation.GetTypeByMetadataName(Constants.Types.FunctionNameAttribute);
21+
var attributeType = compilation.GetTypeByMetadataName(Constants.Types.FunctionNameAttribute);
2222

2323
return attributes.Any(a => attributeType.IsAssignableFrom(a.AttributeClass, true));
2424
}

src/WebJobs.Script.Analyzers/WebJobs.Script.Analyzers.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
<PackageIcon>functions.png</PackageIcon>
1111
<PackageTags>Azure Functions, analyzers</PackageTags>
1212
<TargetFramework>netstandard2.0</TargetFramework>
13-
<Version>1.0.0</Version>
14-
<MajorMinorProductVersion>1.0</MajorMinorProductVersion>
13+
<Version>1.1.0</Version>
14+
<MajorMinorProductVersion>1.1</MajorMinorProductVersion>
1515
<AssemblyVersion>$(MajorMinorProductVersion).0.0</AssemblyVersion>
16-
<FileVersion>1.0.0.0</FileVersion>
16+
<FileVersion>1.1.0.0</FileVersion>
1717
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
1818
<IncludeBuildOutput>false</IncludeBuildOutput>
1919
<SignAssembly>true</SignAssembly>
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using Xunit;
5+
using AnalyzerTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest<Microsoft.Azure.Functions.Analyzers.AvoidNonStaticHttpClientAnalyzer, Microsoft.CodeAnalysis.Testing.Verifiers.XUnitVerifier>;
6+
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.AnalyzerVerifier<Microsoft.Azure.Functions.Analyzers.AvoidNonStaticHttpClientAnalyzer>;
7+
using System.Threading.Tasks;
8+
using Microsoft.CodeAnalysis.Testing;
9+
using System.Collections.Immutable;
10+
11+
namespace WebJobs.Script.Tests.Analyzers
12+
{
13+
public class AvoidNonStaticHttpClientAnalyzerTests
14+
{
15+
[Fact]
16+
public async Task StaticHttpClient_NoDiagnostic()
17+
{
18+
string testCode = @"
19+
using Microsoft.Azure.WebJobs;
20+
using Microsoft.Extensions.Logging;
21+
using System.Net.Http;
22+
23+
namespace FunctionApp
24+
{
25+
public static class SomeFunction
26+
{
27+
public static HttpClient httpClient = new HttpClient();
28+
29+
[FunctionName(nameof(SomeFunction))]
30+
public static void Run([QueueTrigger(""myqueue-items"", Connection = """")] string myQueueItem, ILogger log)
31+
{
32+
httpClient.GetAsync(""https://www.microsoft.com"");
33+
}
34+
}
35+
}
36+
";
37+
38+
var test = new AnalyzerTest();
39+
test.ReferenceAssemblies = ReferenceAssemblies.Net.Net50.WithPackages(ImmutableArray.Create(
40+
new PackageIdentity("Microsoft.NET.Sdk.Functions", "3.0.11"),
41+
new PackageIdentity("Microsoft.Azure.WebJobs.Extensions.Storage", "3.0.10")));
42+
43+
test.TestCode = testCode;
44+
45+
// 0 diagnostics expected
46+
47+
await test.RunAsync();
48+
}
49+
50+
51+
[Fact]
52+
public async Task LocalHttpClientVariable_Diagnostic()
53+
{
54+
string testCode = @"
55+
using Microsoft.Azure.WebJobs;
56+
using Microsoft.Extensions.Logging;
57+
using System.Net.Http;
58+
59+
namespace FunctionApp
60+
{
61+
public static class SomeFunction
62+
{
63+
[FunctionName(nameof(SomeFunction))]
64+
public static void Run([QueueTrigger(""myqueue-items"", Connection = """")] string myQueueItem, ILogger log)
65+
{
66+
var httpClient = new HttpClient();
67+
}
68+
}
69+
}
70+
";
71+
72+
var test = new AnalyzerTest();
73+
test.ReferenceAssemblies = ReferenceAssemblies.Net.Net50.WithPackages(ImmutableArray.Create(
74+
new PackageIdentity("Microsoft.NET.Sdk.Functions", "3.0.11"),
75+
new PackageIdentity("Microsoft.Azure.WebJobs.Extensions.Storage", "3.0.10")));
76+
77+
test.TestCode = testCode;
78+
79+
test.ExpectedDiagnostics.Add(Verify.Diagnostic()
80+
.WithSeverity(Microsoft.CodeAnalysis.DiagnosticSeverity.Warning)
81+
.WithSpan(13, 34, 13, 50));
82+
83+
await test.RunAsync();
84+
}
85+
86+
[Fact]
87+
public async Task LocalNestedHttpClientVariable_Diagnostic()
88+
{
89+
string testCode = @"
90+
using Microsoft.Azure.WebJobs;
91+
using Microsoft.Extensions.Logging;
92+
using System.Net.Http;
93+
94+
namespace FunctionApp
95+
{
96+
public static class SomeFunction
97+
{
98+
[FunctionName(nameof(SomeFunction))]
99+
public static void Run([QueueTrigger(""myqueue-items"", Connection = """")] string myQueueItem, ILogger log)
100+
{
101+
if (true)
102+
{
103+
using (var httpClient = new HttpClient())
104+
{
105+
httpClient.GetAsync(""https://www.microsoft.com"");
106+
}
107+
}
108+
}
109+
}
110+
}
111+
";
112+
113+
var test = new AnalyzerTest();
114+
test.ReferenceAssemblies = ReferenceAssemblies.Net.Net50.WithPackages(ImmutableArray.Create(
115+
new PackageIdentity("Microsoft.NET.Sdk.Functions", "3.0.11"),
116+
new PackageIdentity("Microsoft.Azure.WebJobs.Extensions.Storage", "3.0.10")));
117+
118+
test.TestCode = testCode;
119+
120+
test.ExpectedDiagnostics.Add(Verify.Diagnostic()
121+
.WithSeverity(Microsoft.CodeAnalysis.DiagnosticSeverity.Warning)
122+
.WithSpan(15, 41, 15, 57));
123+
124+
await test.RunAsync();
125+
}
126+
127+
[Fact]
128+
public async Task MethodArgument_Diagnostic()
129+
{
130+
string testCode = @"
131+
using Microsoft.Azure.WebJobs;
132+
using Microsoft.Extensions.Logging;
133+
using System.Net.Http;
134+
using System.Threading.Tasks;
135+
136+
namespace FunctionApp
137+
{
138+
public static class SomeFunction
139+
{
140+
[FunctionName(nameof(SomeFunction))]
141+
public static void Run([QueueTrigger(""myqueue-items"", Connection = """")] string myQueueItem, ILogger log)
142+
{
143+
CallHttp(new HttpClient());
144+
}
145+
private static Task<HttpResponseMessage> CallHttp(HttpClient httpClient)
146+
{
147+
return httpClient.GetAsync(""https://www.microsoft.com"");
148+
}
149+
}
150+
}
151+
";
152+
153+
var test = new AnalyzerTest();
154+
test.ReferenceAssemblies = ReferenceAssemblies.Net.Net50.WithPackages(ImmutableArray.Create(
155+
new PackageIdentity("Microsoft.NET.Sdk.Functions", "3.0.11"),
156+
new PackageIdentity("Microsoft.Azure.WebJobs.Extensions.Storage", "3.0.10")));
157+
158+
test.TestCode = testCode;
159+
160+
test.ExpectedDiagnostics.Add(Verify.Diagnostic()
161+
.WithSeverity(Microsoft.CodeAnalysis.DiagnosticSeverity.Warning)
162+
.WithSpan(14, 26, 14, 42));
163+
164+
await test.RunAsync();
165+
}
166+
167+
[Fact]
168+
public async Task HttpClientDerivedClass_Diagnostic()
169+
{
170+
string testCode = @"
171+
using Microsoft.Azure.WebJobs;
172+
using Microsoft.Extensions.Logging;
173+
using System.Net.Http;
174+
175+
namespace FunctionApp
176+
{
177+
public static class SomeFunction
178+
{
179+
[FunctionName(nameof(SomeFunction))]
180+
public static void Run([QueueTrigger(""myqueue-items"", Connection = """")]string myQueueItem, ILogger log)
181+
{
182+
var httpClient = new CustomHttpClient();
183+
httpClient.GetAsync(""https://www.microsoft.com"");
184+
}
185+
}
186+
187+
public class CustomHttpClient : HttpClient { }
188+
}
189+
";
190+
191+
var test = new AnalyzerTest();
192+
test.ReferenceAssemblies = ReferenceAssemblies.Net.Net50.WithPackages(ImmutableArray.Create(
193+
new PackageIdentity("Microsoft.NET.Sdk.Functions", "3.0.11"),
194+
new PackageIdentity("Microsoft.Azure.WebJobs.Extensions.Storage", "3.0.10")));
195+
196+
test.TestCode = testCode;
197+
198+
test.ExpectedDiagnostics.Add(Verify.Diagnostic()
199+
.WithSeverity(Microsoft.CodeAnalysis.DiagnosticSeverity.Warning)
200+
.WithSpan(13, 30, 13, 52));
201+
202+
await test.RunAsync();
203+
}
204+
}
205+
}

0 commit comments

Comments
 (0)